@canonry/canonry 7.19.0 → 7.20.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 (32) hide show
  1. package/assets/agent-workspace/skills/aero/references/agent-operations.md +14 -0
  2. package/assets/agent-workspace/skills/canonry/SKILL.md +14 -0
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +33 -1
  4. package/assets/assets/{AuditHistoryPanel-DEEWm-4t.js → AuditHistoryPanel-C0vj3liH.js} +1 -1
  5. package/assets/assets/{BacklinksPage-F5QBgXbF.js → BacklinksPage-TnDsI9Wu.js} +1 -1
  6. package/assets/assets/{HistoryPage-CNp42sUO.js → HistoryPage-DEEC3g6M.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-XpI9hVMb.js → MeasurementPropertyPage-MT4tFOwx.js} +1 -1
  8. package/assets/assets/{ProjectPage-B_V4QpyI.js → ProjectPage-NI-bjpfN.js} +1 -1
  9. package/assets/assets/{RunsPage-PTHloZl3.js → RunsPage-GIKqPu0J.js} +1 -1
  10. package/assets/assets/{SettingsPage-C-aDo2zf.js → SettingsPage-DLFwSj4J.js} +1 -1
  11. package/assets/assets/{SiteHealthSection-DvoT8IuX.js → SiteHealthSection-CYG9IMh5.js} +3 -3
  12. package/assets/assets/{TrafficPage-DahJoMKF.js → TrafficPage-oOxDmxQq.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-BGlHXOlk.js → TrafficSourceDetailPage-BGrwpVvM.js} +1 -1
  14. package/assets/assets/{extract-error-message-CP_fyJLM.js → extract-error-message-DRDQFsgG.js} +1 -1
  15. package/assets/assets/{index-C5V8xlr0.js → index-B_2gPTCa.js} +20 -20
  16. package/assets/assets/{react-sigma_core.esm.min-DUS8srfg.js → react-sigma_core.esm.min-DzDqz7hq.js} +1 -1
  17. package/assets/assets/{v2-overview-adapter-ycJ1guZb.js → v2-overview-adapter-BHzFliuj.js} +1 -1
  18. package/assets/index.html +1 -1
  19. package/dist/{chunk-A75L7CUV.js → chunk-222NAFTY.js} +2 -2
  20. package/dist/{chunk-UUI4HOEL.js → chunk-46JZR2IV.js} +46 -8
  21. package/dist/{chunk-VSTKTQPS.js → chunk-5IXQAOVK.js} +16 -1
  22. package/dist/{chunk-PBTTPZZE.js → chunk-6H2BC5EY.js} +3 -3
  23. package/dist/{chunk-OMP5OLBK.js → chunk-KF7LRHGJ.js} +368 -211
  24. package/dist/{chunk-CYHUZ2JQ.js → chunk-MRKV43TV.js} +19 -0
  25. package/dist/{chunk-35IIFDIT.js → chunk-NDQOCFJQ.js} +61 -43
  26. package/dist/cli.js +213 -39
  27. package/dist/{demo-server-K5G54GOE.js → demo-server-32PU7BM6.js} +4 -4
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-LV2TQMMR.js → intelligence-service-256DQBVR.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +10 -10
@@ -20,20 +20,20 @@ import {
20
20
  saveConfig,
21
21
  saveConfigPatch,
22
22
  sentimentInstallReadiness
23
- } from "./chunk-35IIFDIT.js";
23
+ } from "./chunk-NDQOCFJQ.js";
24
24
  import {
25
25
  computeFactorAverages,
26
26
  deleteSiteCrawlGraphLayout,
27
27
  persistSiteCrawlGraphLayout,
28
28
  prepareSiteCrawlGraphLayout,
29
29
  recordedSharePct
30
- } from "./chunk-A75L7CUV.js";
30
+ } from "./chunk-222NAFTY.js";
31
31
  import {
32
32
  CanonryMcpToolNames,
33
33
  CliError,
34
34
  PACKAGE_VERSION,
35
35
  canonryMcpTools
36
- } from "./chunk-VSTKTQPS.js";
36
+ } from "./chunk-5IXQAOVK.js";
37
37
  import {
38
38
  CC_CACHE_DIR,
39
39
  DUCKDB_SPEC,
@@ -223,13 +223,14 @@ import {
223
223
  toAlertView,
224
224
  usageCounters,
225
225
  writeAuditLog
226
- } from "./chunk-UUI4HOEL.js";
226
+ } from "./chunk-46JZR2IV.js";
227
227
  import {
228
228
  AGENT_MEMORY_VALUE_MAX_BYTES,
229
229
  AGENT_NONE,
230
230
  AGENT_PROVIDER_IDS,
231
231
  AI_ENGINE_DOMAINS,
232
232
  AI_ENGINE_SELF_DOMAINS,
233
+ API_PROVIDER_NAMES,
233
234
  AdsAdGroupBillingEventTypes,
234
235
  AdsCampaignBiddingTypes,
235
236
  AgentProviderIds,
@@ -269,6 +270,7 @@ import {
269
270
  ProviderBatchSubmitError,
270
271
  ProviderDispatchModes,
271
272
  ProviderNames,
273
+ ProviderReloadErrorReasons,
272
274
  RATIO_UNIT_META_KEY,
273
275
  ROUTE_CAPABLE_CITED_URL_PROVIDERS,
274
276
  RUN_FILL_PROVIDER_BREAKER,
@@ -398,6 +400,7 @@ import {
398
400
  notFound,
399
401
  observeTemplateLinkDetection,
400
402
  observeTemplateLinkEdges,
403
+ operationInProgress,
401
404
  parseMeasurementRunManifestV1,
402
405
  parseOriginList,
403
406
  parseRunError,
@@ -439,7 +442,7 @@ import {
439
442
  winnabilityClassLabel,
440
443
  withRetry,
441
444
  withSkippedProviders
442
- } from "./chunk-CYHUZ2JQ.js";
445
+ } from "./chunk-MRKV43TV.js";
443
446
 
444
447
  // src/runtime-context.ts
445
448
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -3569,7 +3572,7 @@ import path8 from "path";
3569
3572
  import { isIP } from "net";
3570
3573
  import { fileURLToPath as fileURLToPath3 } from "url";
3571
3574
  import { setImmediate as yieldToEventLoop4 } from "timers/promises";
3572
- import { and as and23, eq as eq29, isNotNull as isNotNull4, isNull as isNull4, lte as lte2, ne as ne3, or as or3, sql as sql11 } from "drizzle-orm";
3575
+ import { and as and24, eq as eq29, isNotNull as isNotNull4, isNull as isNull4, lte as lte2, ne as ne3, or as or3, sql as sql11 } from "drizzle-orm";
3573
3576
 
3574
3577
  // ../integration-typesafe/src/classifier.ts
3575
3578
  import { createHash } from "crypto";
@@ -8049,7 +8052,7 @@ function nonBlank(value) {
8049
8052
  return trimmed ? trimmed : void 0;
8050
8053
  }
8051
8054
  function resolveBuildCommit(env = process.env) {
8052
- const embedded = true ? "e760bb248ecb01eb5783f35367dbc100edd5ed12" : void 0;
8055
+ const embedded = true ? "f2214811aaec80f02e4d56b9851dc5ad8d5cf9e0" : void 0;
8053
8056
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
8054
8057
  }
8055
8058
  function resolveInstanceIdentity(env = process.env) {
@@ -8205,48 +8208,57 @@ var ProviderExecutionGate = class {
8205
8208
  maxPerMinute;
8206
8209
  window = [];
8207
8210
  waiters = [];
8208
- rateLimitChain = Promise.resolve();
8211
+ rateTimer;
8212
+ dispatching = false;
8209
8213
  inFlight = 0;
8214
+ updatePolicy(maxConcurrency, maxPerMinute) {
8215
+ this.maxConcurrency = maxConcurrency;
8216
+ this.maxPerMinute = maxPerMinute;
8217
+ this.dispatchQueued();
8218
+ }
8210
8219
  async run(task) {
8211
- await this.acquire();
8220
+ return new Promise((resolve2, reject) => {
8221
+ this.waiters.push(() => {
8222
+ void this.execute(task, resolve2, reject);
8223
+ });
8224
+ this.dispatchQueued();
8225
+ });
8226
+ }
8227
+ async execute(task, resolve2, reject) {
8212
8228
  try {
8213
- await this.waitForRateLimit();
8214
- return await task();
8229
+ resolve2(await task());
8230
+ } catch (error) {
8231
+ reject(error);
8215
8232
  } finally {
8216
- this.release();
8233
+ this.inFlight--;
8234
+ this.dispatchQueued();
8217
8235
  }
8218
8236
  }
8219
- async acquire() {
8220
- if (this.inFlight < Math.max(1, this.maxConcurrency)) {
8221
- this.inFlight++;
8222
- return;
8237
+ dispatchQueued() {
8238
+ if (this.dispatching) return;
8239
+ if (this.rateTimer !== void 0) {
8240
+ clearTimeout(this.rateTimer);
8241
+ this.rateTimer = void 0;
8223
8242
  }
8224
- await new Promise((resolve2) => this.waiters.push(resolve2));
8225
- this.inFlight++;
8226
- }
8227
- release() {
8228
- this.inFlight = Math.max(0, this.inFlight - 1);
8229
- this.waiters.shift()?.();
8230
- }
8231
- async waitForRateLimit() {
8232
- let releaseChain;
8233
- const previousChain = this.rateLimitChain;
8234
- this.rateLimitChain = new Promise((resolve2) => {
8235
- releaseChain = resolve2;
8236
- });
8237
- await previousChain;
8243
+ this.dispatching = true;
8238
8244
  try {
8239
- const now = Date.now();
8240
- const windowStart = now - 6e4;
8241
- while (this.window.length > 0 && this.window[0] < windowStart) this.window.shift();
8242
- if (this.window.length >= this.maxPerMinute) {
8243
- await new Promise((resolve2) => setTimeout(resolve2, this.window[0] + 6e4 - now + 50));
8244
- const nextWindowStart = Date.now() - 6e4;
8245
- while (this.window.length > 0 && this.window[0] < nextWindowStart) this.window.shift();
8246
- }
8247
- this.window.push(Date.now());
8245
+ while (this.waiters.length > 0 && this.inFlight < Math.max(1, this.maxConcurrency)) {
8246
+ const now = Date.now();
8247
+ const windowStart = now - 6e4;
8248
+ while (this.window.length > 0 && this.window[0] < windowStart) this.window.shift();
8249
+ if (this.window.length >= this.maxPerMinute) break;
8250
+ this.window.push(now);
8251
+ this.inFlight++;
8252
+ this.waiters.shift()();
8253
+ }
8248
8254
  } finally {
8249
- releaseChain?.();
8255
+ this.dispatching = false;
8256
+ }
8257
+ if (this.waiters.length > 0 && this.inFlight < Math.max(1, this.maxConcurrency)) {
8258
+ this.rateTimer = setTimeout(() => {
8259
+ this.rateTimer = void 0;
8260
+ this.dispatchQueued();
8261
+ }, this.window[0] + 6e4 - Date.now() + 50);
8250
8262
  }
8251
8263
  }
8252
8264
  };
@@ -8259,6 +8271,9 @@ function getSharedProviderExecutionGate(providerName, maxConcurrency, maxPerMinu
8259
8271
  sharedGates.set(key, gate);
8260
8272
  return gate;
8261
8273
  }
8274
+ function updateSharedProviderExecutionGate(providerName, maxConcurrency, maxPerMinute) {
8275
+ getSharedProviderExecutionGate(providerName, maxConcurrency, maxPerMinute).updatePolicy(maxConcurrency, maxPerMinute);
8276
+ }
8262
8277
 
8263
8278
  // src/usage-quota.ts
8264
8279
  import crypto6 from "crypto";
@@ -8576,6 +8591,7 @@ var JobRunner = class {
8576
8591
  * handoff across a restart.
8577
8592
  */
8578
8593
  executing = /* @__PURE__ */ new Map();
8594
+ executingBatchRegistrations = /* @__PURE__ */ new Map();
8579
8595
  constructor(db, registry, opts) {
8580
8596
  this.onFirstActivation = opts?.onFirstActivation;
8581
8597
  this.db = db;
@@ -8596,6 +8612,12 @@ var JobRunner = class {
8596
8612
  isExecuting(runId) {
8597
8613
  return this.executing.has(runId);
8598
8614
  }
8615
+ /** Reload must preserve credentials captured by a batch sweep until it settles. */
8616
+ getExecutingBatchRegistrations() {
8617
+ return [...this.executingBatchRegistrations.values()].flatMap(
8618
+ ({ runId, registrations }) => registrations.map((registration) => ({ runId, registration }))
8619
+ );
8620
+ }
8599
8621
  hasProviderBatches(runId) {
8600
8622
  return this.db.select({ id: providerBatches.id }).from(providerBatches).where(eq3(providerBatches.runId, runId)).limit(1).get() !== void 0;
8601
8623
  }
@@ -8711,6 +8733,7 @@ var JobRunner = class {
8711
8733
  const providerReservations = /* @__PURE__ */ new Map();
8712
8734
  const batchRowIds = [];
8713
8735
  const releaseExecution = this.enterExecution(runId);
8736
+ const batchExecution = Symbol(runId);
8714
8737
  try {
8715
8738
  const existingRun = this.getRunState(runId);
8716
8739
  if (!existingRun) {
@@ -8829,6 +8852,18 @@ var JobRunner = class {
8829
8852
  let totalSnapshotsInserted = 0;
8830
8853
  const apiProviders = calledProviders.filter((p) => !isBrowserProvider(p.adapter.name));
8831
8854
  const browserProviders = calledProviders.filter((p) => isBrowserProvider(p.adapter.name));
8855
+ const providerKey = (provider) => provider.adapter.name.trim().toLocaleLowerCase("en");
8856
+ const batchProviders = /* @__PURE__ */ new Set();
8857
+ if (planExecution) {
8858
+ const frozenModes = existingRun.providerDispatchModes ?? {};
8859
+ for (const provider of apiProviders) {
8860
+ if (frozenModes[providerKey(provider)] !== ProviderDispatchModes.batch) continue;
8861
+ if (adapterSupportsBatch(provider.adapter)) batchProviders.add(provider.adapter.name);
8862
+ else log2.warn("run.batch-unsupported", { runId, provider: provider.adapter.name });
8863
+ }
8864
+ const registrations = apiProviders.filter((provider) => batchProviders.has(provider.adapter.name));
8865
+ if (registrations.length > 0) this.executingBatchRegistrations.set(batchExecution, { runId, registrations });
8866
+ }
8832
8867
  const processQueryForProvider = async (registeredProvider, q) => {
8833
8868
  const { adapter, config } = registeredProvider;
8834
8869
  const providerName = adapter.name;
@@ -9040,15 +9075,7 @@ var JobRunner = class {
9040
9075
  providerCallStart = Date.now();
9041
9076
  if (planExecution) {
9042
9077
  const plan = planExecution;
9043
- const providerKey = (provider) => provider.adapter.name.trim().toLocaleLowerCase("en");
9044
9078
  const unitsFor = (provider) => plan.unitsByProvider.get(providerKey(provider)) ?? [];
9045
- const frozenModes = existingRun.providerDispatchModes ?? {};
9046
- const batchProviders = /* @__PURE__ */ new Set();
9047
- for (const provider of apiProviders) {
9048
- if (frozenModes[providerKey(provider)] !== ProviderDispatchModes.batch) continue;
9049
- if (adapterSupportsBatch(provider.adapter)) batchProviders.add(provider.adapter.name);
9050
- else log2.warn("run.batch-unsupported", { runId, provider: provider.adapter.name });
9051
- }
9052
9079
  log2.info("run.plan-dispatch", {
9053
9080
  runId,
9054
9081
  expectedSlots: plan.manifest.expectedSlots.length,
@@ -9065,7 +9092,16 @@ var JobRunner = class {
9065
9092
  batchRowIds
9066
9093
  };
9067
9094
  await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
9068
- const units = batchProviders.has(registeredProvider.adapter.name) ? await this.submitPlanBatches(submission, registeredProvider, unitsFor(registeredProvider)) : unitsFor(registeredProvider);
9095
+ let units;
9096
+ if (batchProviders.has(registeredProvider.adapter.name)) {
9097
+ const submitExecution = Symbol(runId);
9098
+ this.executingBatchRegistrations.set(submitExecution, { runId, registrations: [registeredProvider] });
9099
+ try {
9100
+ units = await this.submitPlanBatches(submission, registeredProvider, unitsFor(registeredProvider));
9101
+ } finally {
9102
+ this.executingBatchRegistrations.delete(submitExecution);
9103
+ }
9104
+ } else units = unitsFor(registeredProvider);
9069
9105
  await Promise.all(units.map(async (unit) => {
9070
9106
  await processNodeForProvider(registeredProvider, unit);
9071
9107
  }));
@@ -9197,6 +9233,7 @@ var JobRunner = class {
9197
9233
  }
9198
9234
  await this.cancelAtProvider(abandoned);
9199
9235
  } finally {
9236
+ this.executingBatchRegistrations.delete(batchExecution);
9200
9237
  releaseExecution();
9201
9238
  }
9202
9239
  }
@@ -9647,9 +9684,9 @@ var JobRunner = class {
9647
9684
  return marked;
9648
9685
  }
9649
9686
  async cancelAtProvider(rows) {
9650
- for (const row of rows) {
9687
+ const registrations = rows.map((row) => ({ row, registered: this.registry.get(row.provider) }));
9688
+ for (const { row, registered } of registrations) {
9651
9689
  if (!row.providerBatchId) continue;
9652
- const registered = this.registry.get(row.provider);
9653
9690
  if (!registered?.adapter.batch) {
9654
9691
  log2.warn("batch.cancel-unavailable", { batchId: row.id, provider: row.provider });
9655
9692
  continue;
@@ -14568,11 +14605,29 @@ function extractStoredAnswerAnchors(provider, rawResponse) {
14568
14605
  }
14569
14606
 
14570
14607
  // src/provider-registry.ts
14608
+ function resolveRegistration(adapter, config) {
14609
+ return {
14610
+ adapter,
14611
+ config: config.model === void 0 ? config : { ...config, model: resolveProviderModel(adapter.name, config.model) }
14612
+ };
14613
+ }
14571
14614
  var ProviderRegistry = class {
14572
14615
  providers = /* @__PURE__ */ new Map();
14573
14616
  register(adapter, config) {
14574
- const resolved = config.model === void 0 ? config : { ...config, model: resolveProviderModel(adapter.name, config.model) };
14575
- this.providers.set(adapter.name, { adapter, config: resolved });
14617
+ const provider = resolveRegistration(adapter, config);
14618
+ this.providers.set(adapter.name, provider);
14619
+ updateSharedProviderExecutionGate(adapter.name, provider.config.quotaPolicy.maxConcurrency, provider.config.quotaPolicy.maxRequestsPerMinute);
14620
+ }
14621
+ /** Publish a complete registration set without mutating configs captured by active runs. */
14622
+ replace(providers) {
14623
+ const next = /* @__PURE__ */ new Map();
14624
+ for (const { adapter, config } of providers) {
14625
+ next.set(adapter.name, resolveRegistration(adapter, config));
14626
+ }
14627
+ this.providers = next;
14628
+ for (const { adapter, config } of next.values()) {
14629
+ updateSharedProviderExecutionGate(adapter.name, config.quotaPolicy.maxConcurrency, config.quotaPolicy.maxRequestsPerMinute);
14630
+ }
14576
14631
  }
14577
14632
  get(name) {
14578
14633
  return this.providers.get(name);
@@ -14619,6 +14674,96 @@ var ProviderRegistry = class {
14619
14674
  }
14620
14675
  };
14621
14676
 
14677
+ // src/provider-registration.ts
14678
+ function isApiProviderRegistrable(name, entry) {
14679
+ if (!entry) return false;
14680
+ if (name === ProviderNames.local) return Boolean(entry.baseUrl);
14681
+ if (name === ProviderNames.gemini) return Boolean(entry.apiKey || entry.vertexProject);
14682
+ return Boolean(entry.apiKey);
14683
+ }
14684
+ function isCdpProviderRegistrable(cdp) {
14685
+ return Boolean(cdp?.host || cdp?.port);
14686
+ }
14687
+ function registeredProviderNames(config) {
14688
+ const names = API_PROVIDER_NAMES.filter((name) => {
14689
+ const entry = config.providers?.[name];
14690
+ if (name === ProviderNames.gemini && !entry && config.geminiApiKey) return true;
14691
+ return isApiProviderRegistrable(name, entry);
14692
+ });
14693
+ if (isCdpProviderRegistrable(config.cdp)) names.push(ProviderNames.cdpChatgpt);
14694
+ return names;
14695
+ }
14696
+
14697
+ // src/provider-runtime-config.ts
14698
+ var DEFAULT_PROVIDER_QUOTA = {
14699
+ maxConcurrency: 2,
14700
+ maxRequestsPerMinute: 10,
14701
+ maxRequestsPerDay: 1e3
14702
+ };
14703
+ var DEFAULT_CDP_QUOTA = {
14704
+ maxConcurrency: 1,
14705
+ maxRequestsPerMinute: 4,
14706
+ maxRequestsPerDay: 200
14707
+ };
14708
+ function resolveProviderQuotaPolicy(quota, defaults) {
14709
+ const saved = quota && typeof quota === "object" && !Array.isArray(quota) ? quota : {};
14710
+ return {
14711
+ maxConcurrency: saved.maxConcurrency ?? defaults.maxConcurrency,
14712
+ maxRequestsPerMinute: saved.maxRequestsPerMinute ?? defaults.maxRequestsPerMinute,
14713
+ maxRequestsPerDay: saved.maxRequestsPerDay ?? defaults.maxRequestsPerDay
14714
+ };
14715
+ }
14716
+ function configuredProviderEntries(config, apiAdapters, cdpAdapter) {
14717
+ const registered = [];
14718
+ for (const adapter of apiAdapters) {
14719
+ const entry = config.providers?.[adapter.name];
14720
+ if (!entry) continue;
14721
+ if (isApiProviderRegistrable(adapter.name, entry)) {
14722
+ registered.push(resolveRegistration(adapter, providerConfigFromEntry(
14723
+ adapter.name,
14724
+ entry,
14725
+ resolveProviderQuotaPolicy(entry.quota, DEFAULT_PROVIDER_QUOTA)
14726
+ )));
14727
+ }
14728
+ }
14729
+ if (isCdpProviderRegistrable(config.cdp)) {
14730
+ registered.push(resolveRegistration(cdpAdapter, {
14731
+ provider: cdpAdapter.name,
14732
+ cdpEndpoint: `ws://${config.cdp?.host ?? "localhost"}:${config.cdp?.port ?? 9222}`,
14733
+ quotaPolicy: resolveProviderQuotaPolicy(config.cdp?.quota, DEFAULT_CDP_QUOTA)
14734
+ }));
14735
+ }
14736
+ return registered;
14737
+ }
14738
+
14739
+ // src/provider-reload-batch-guard.ts
14740
+ import { isDeepStrictEqual } from "util";
14741
+ import { and as and15, inArray as inArray7 } from "drizzle-orm";
14742
+ function pendingBatchIdentity(provider) {
14743
+ const { quotaPolicy: _quotaPolicy, ...identity } = resolveRegistration(provider.adapter, provider.config).config;
14744
+ return identity;
14745
+ }
14746
+ function assertProviderReloadKeepsPendingBatches(db, previous, next, executing = []) {
14747
+ const nextByName = new Map(next.map((provider) => [provider.adapter.name, provider]));
14748
+ const changedRegistration = (provider) => {
14749
+ const replacement = nextByName.get(provider.adapter.name);
14750
+ return !replacement || !isDeepStrictEqual(pendingBatchIdentity(provider), pendingBatchIdentity(replacement));
14751
+ };
14752
+ const changed = previous.filter(changedRegistration).map((provider) => provider.adapter.name);
14753
+ const active = executing.filter((snapshot) => changedRegistration(snapshot.registration));
14754
+ if (changed.length === 0 && active.length === 0) return;
14755
+ const pending = changed.length === 0 ? [] : db.select({ id: providerBatches.id, provider: providerBatches.provider, runId: providerBatches.runId }).from(providerBatches).where(and15(
14756
+ inArray7(providerBatches.provider, changed),
14757
+ inArray7(providerBatches.status, [ProviderBatchStatuses.submitting, ProviderBatchStatuses.submitted, ProviderBatchStatuses.ended])
14758
+ )).all();
14759
+ if (pending.length === 0 && active.length === 0) return;
14760
+ throw operationInProgress("Changes to provider credentials or execution settings must wait for outstanding batch work to settle.", {
14761
+ providers: [.../* @__PURE__ */ new Set([...pending.map((batch) => batch.provider), ...active.map((snapshot) => snapshot.registration.adapter.name)])].sort(),
14762
+ batchIds: pending.map((batch) => batch.id).sort(),
14763
+ runIds: [.../* @__PURE__ */ new Set([...pending.map((batch) => batch.runId), ...active.map((snapshot) => snapshot.runId)])].sort()
14764
+ });
14765
+ }
14766
+
14622
14767
  // src/provider-model-catalog.ts
14623
14768
  import { createHash as createHash4 } from "crypto";
14624
14769
  var TTL_MS = 60 * 60 * 1e3;
@@ -14682,7 +14827,7 @@ function createProviderModelCatalog(registry) {
14682
14827
  // src/scheduler.ts
14683
14828
  import crypto21 from "crypto";
14684
14829
  import cron from "node-cron";
14685
- import { and as and15, eq as eq17, inArray as inArray7, notExists, sql as sql7 } from "drizzle-orm";
14830
+ import { and as and16, eq as eq17, inArray as inArray8, notExists, sql as sql7 } from "drizzle-orm";
14686
14831
  var log16 = createLogger("Scheduler");
14687
14832
  var DEFAULT_HEALTH_CRON = "0 */6 * * *";
14688
14833
  function ensureDefaultHealthSchedule(db, projectId, now = (/* @__PURE__ */ new Date()).toISOString()) {
@@ -14733,7 +14878,7 @@ var Scheduler = class {
14733
14878
  */
14734
14879
  ensureHealthSchedules() {
14735
14880
  const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
14736
- this.db.select({ one: sql7`1` }).from(schedules).where(and15(
14881
+ this.db.select({ one: sql7`1` }).from(schedules).where(and16(
14737
14882
  eq17(schedules.projectId, projects.id),
14738
14883
  eq17(schedules.kind, SchedulableRunKinds.doctor)
14739
14884
  ))
@@ -14812,7 +14957,7 @@ var Scheduler = class {
14812
14957
  this.stopTask(key, existing, "Stopped");
14813
14958
  this.tasks.delete(key);
14814
14959
  }
14815
- const schedule = this.db.select().from(schedules).where(and15(eq17(schedules.projectId, projectId), eq17(schedules.kind, kind))).get();
14960
+ const schedule = this.db.select().from(schedules).where(and16(eq17(schedules.projectId, projectId), eq17(schedules.kind, kind))).get();
14816
14961
  if (schedule && schedule.enabled) {
14817
14962
  this.registerCronTask(schedule);
14818
14963
  }
@@ -14948,7 +15093,7 @@ var Scheduler = class {
14948
15093
  const result2 = tx.update(schedules).set({
14949
15094
  nextRunAt,
14950
15095
  updatedAt: nextScheduleUpdatedAt(current.updatedAt)
14951
- }).where(and15(
15096
+ }).where(and16(
14952
15097
  eq17(schedules.id, current.id),
14953
15098
  eq17(schedules.enabled, true),
14954
15099
  eq17(schedules.nextRunAt, dueAt),
@@ -15027,10 +15172,10 @@ var Scheduler = class {
15027
15172
  log16.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
15028
15173
  return;
15029
15174
  }
15030
- const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and15(
15175
+ const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and16(
15031
15176
  eq17(runs.projectId, projectId),
15032
15177
  eq17(runs.kind, RunKinds["ads-sync"]),
15033
- inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
15178
+ inArray8(runs.status, [RunStatuses.queued, RunStatuses.running])
15034
15179
  )).get();
15035
15180
  if (activeAdsRun) {
15036
15181
  log16.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
@@ -15098,10 +15243,10 @@ var Scheduler = class {
15098
15243
  log16.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
15099
15244
  return;
15100
15245
  }
15101
- const active = this.db.select({ id: runs.id }).from(runs).where(and15(
15246
+ const active = this.db.select({ id: runs.id }).from(runs).where(and16(
15102
15247
  eq17(runs.projectId, projectId),
15103
15248
  eq17(runs.kind, RunKinds["site-audit"]),
15104
- inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
15249
+ inArray8(runs.status, [RunStatuses.queued, RunStatuses.running])
15105
15250
  )).get();
15106
15251
  if (active) {
15107
15252
  log16.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
@@ -15276,7 +15421,7 @@ function startSiteLivenessLoop(deps, intervalMs = SITE_LIVENESS_INTERVAL_MS) {
15276
15421
  }
15277
15422
 
15278
15423
  // src/provider-batch-poller.ts
15279
- import { and as and16, eq as eq18, inArray as inArray8, isNotNull as isNotNull2, isNull as isNull2, notExists as notExists2, sql as sql8 } from "drizzle-orm";
15424
+ import { and as and17, eq as eq18, inArray as inArray9, isNotNull as isNotNull2, isNull as isNull2, notExists as notExists2, sql as sql8 } from "drizzle-orm";
15280
15425
  var log18 = createLogger("ProviderBatchPoller");
15281
15426
  var PROVIDER_BATCH_POLL_INITIAL_MS = 3e4;
15282
15427
  var PROVIDER_BATCH_POLL_MAX_MS = 10 * 6e4;
@@ -15293,7 +15438,7 @@ var ProviderBatchPoller = class {
15293
15438
  /** One pass: advance every outstanding batch that is due, then finalize what that settled. */
15294
15439
  async tick() {
15295
15440
  const { db } = this.deps;
15296
- const rows = db.select().from(providerBatches).where(inArray8(providerBatches.status, [...OUTSTANDING_PROVIDER_BATCH_STATUSES])).all();
15441
+ const rows = db.select().from(providerBatches).where(inArray9(providerBatches.status, [...OUTSTANDING_PROVIDER_BATCH_STATUSES])).all();
15297
15442
  const live = new Set(rows.map((row) => row.id));
15298
15443
  for (const id of this.schedule.keys()) if (!live.has(id)) this.schedule.delete(id);
15299
15444
  const settledRuns = /* @__PURE__ */ new Map();
@@ -15336,7 +15481,7 @@ var ProviderBatchPoller = class {
15336
15481
  status = poll.status;
15337
15482
  if (poll.status === "ended") {
15338
15483
  const endedAt = poll.endedAt ?? new Date(this.now()).toISOString();
15339
- const ended = this.deps.db.update(providerBatches).set({ status: ProviderBatchStatuses.ended, endedAt, resultsExpireAt: poll.resultsExpireAt ?? null, updatedAt: endedAt }).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15484
+ const ended = this.deps.db.update(providerBatches).set({ status: ProviderBatchStatuses.ended, endedAt, resultsExpireAt: poll.resultsExpireAt ?? null, updatedAt: endedAt }).where(and17(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15340
15485
  this.schedule.delete(row.id);
15341
15486
  return ended ? this.ingest({ ...row, status: ProviderBatchStatuses.ended, resultsExpireAt: poll.resultsExpireAt ?? null }) : false;
15342
15487
  }
@@ -15360,7 +15505,7 @@ var ProviderBatchPoller = class {
15360
15505
  const at = new Date(now).toISOString();
15361
15506
  if (row.cancelRequestedAt === null) {
15362
15507
  if (now < Date.parse(row.deadlineAt)) return false;
15363
- const claimed = this.deps.db.update(providerBatches).set({ cancelRequestedAt: at, updatedAt: at }).where(and16(
15508
+ const claimed = this.deps.db.update(providerBatches).set({ cancelRequestedAt: at, updatedAt: at }).where(and17(
15364
15509
  eq18(providerBatches.id, row.id),
15365
15510
  eq18(providerBatches.status, ProviderBatchStatuses.submitted),
15366
15511
  isNull2(providerBatches.cancelRequestedAt)
@@ -15381,7 +15526,7 @@ var ProviderBatchPoller = class {
15381
15526
  status: ProviderBatchStatuses.cancelled,
15382
15527
  error: `The provider batch did not end within an hour of being cancelled at its deadline; its ${row.requestCount} answer(s) were not recorded.`,
15383
15528
  updatedAt: at
15384
- }).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15529
+ }).where(and17(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15385
15530
  this.schedule.delete(row.id);
15386
15531
  if (gaveUp) log18.warn("batch.abandoned-after-deadline", { batchId: row.id, runId: row.runId, provider: row.provider });
15387
15532
  return gaveUp;
@@ -15421,10 +15566,10 @@ var ProviderBatchPoller = class {
15421
15566
  /** Runs the sweep handed off whose batches have all settled. */
15422
15567
  finalizableRuns() {
15423
15568
  const holding = [ProviderBatchStatuses.submitting, ...OUTSTANDING_PROVIDER_BATCH_STATUSES];
15424
- return this.deps.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and16(
15569
+ return this.deps.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and17(
15425
15570
  eq18(runs.status, RunStatuses.running),
15426
15571
  isNotNull2(runs.pendingProviderErrors),
15427
- notExists2(this.deps.db.select({ one: sql8`1` }).from(providerBatches).where(and16(eq18(providerBatches.runId, runs.id), inArray8(providerBatches.status, holding))))
15572
+ notExists2(this.deps.db.select({ one: sql8`1` }).from(providerBatches).where(and17(eq18(providerBatches.runId, runs.id), inArray9(providerBatches.status, holding))))
15428
15573
  )).all();
15429
15574
  }
15430
15575
  };
@@ -15495,7 +15640,7 @@ async function refreshAllIntegrations(client, projectName, now = /* @__PURE__ */
15495
15640
  }
15496
15641
 
15497
15642
  // src/notifier.ts
15498
- import { eq as eq19, desc as desc8, and as and17, inArray as inArray9, or } from "drizzle-orm";
15643
+ import { eq as eq19, desc as desc8, and as and18, inArray as inArray10, or } from "drizzle-orm";
15499
15644
  import crypto22 from "crypto";
15500
15645
  var log20 = createLogger("Notifier");
15501
15646
  var MATERIAL_MAGNITUDE_DELTA = 20;
@@ -15880,8 +16025,8 @@ var Notifier = class {
15880
16025
  }
15881
16026
  }
15882
16027
  async dispatchQueuedReviewAlerts(projectId) {
15883
- const pendingReviews = this.db.select().from(gbpReviews).where(and17(eq19(gbpReviews.projectId, projectId), eq19(gbpReviews.alertState, "pending"))).all();
15884
- const pendingRatings = this.db.select().from(gbpReviewRatings).where(and17(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.alertState, "pending"))).all();
16028
+ const pendingReviews = this.db.select().from(gbpReviews).where(and18(eq19(gbpReviews.projectId, projectId), eq19(gbpReviews.alertState, "pending"))).all();
16029
+ const pendingRatings = this.db.select().from(gbpReviewRatings).where(and18(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.alertState, "pending"))).all();
15885
16030
  if (pendingReviews.length === 0 && pendingRatings.length === 0) return { reviews: 0, ratingDrops: 0, delivered: 0 };
15886
16031
  const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
15887
16032
  if (!project) return { reviews: 0, ratingDrops: 0, delivered: 0 };
@@ -15897,7 +16042,7 @@ var Notifier = class {
15897
16042
  }).from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).all().map((loc) => [loc.locationName, loc])
15898
16043
  );
15899
16044
  const v4HasWorked = new Set(
15900
- this.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and17(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
16045
+ this.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and18(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
15901
16046
  );
15902
16047
  const coveredByBusinessProfile = (locationName) => businessProfileReviewsCover(locations.get(locationName)?.reviewsAccess ?? null, v4HasWorked.has(locationName));
15903
16048
  const locationRef = (locationName) => {
@@ -15906,11 +16051,11 @@ var Notifier = class {
15906
16051
  };
15907
16052
  const setReviewState = (ids, alertState) => {
15908
16053
  if (ids.length === 0) return;
15909
- this.db.update(gbpReviews).set({ alertState, alertStateAt: nowIso }).where(inArray9(gbpReviews.id, ids)).run();
16054
+ this.db.update(gbpReviews).set({ alertState, alertStateAt: nowIso }).where(inArray10(gbpReviews.id, ids)).run();
15910
16055
  };
15911
16056
  const setRatingState = (ids, alertState) => {
15912
16057
  if (ids.length === 0) return;
15913
- this.db.update(gbpReviewRatings).set({ alertState, alertStateAt: nowIso }).where(inArray9(gbpReviewRatings.id, ids)).run();
16058
+ this.db.update(gbpReviewRatings).set({ alertState, alertStateAt: nowIso }).where(inArray10(gbpReviewRatings.id, ids)).run();
15914
16059
  };
15915
16060
  const reviewsToSend = [];
15916
16061
  for (const row of pendingReviews) {
@@ -16012,7 +16157,7 @@ var Notifier = class {
16012
16157
  });
16013
16158
  return [];
16014
16159
  }
16015
- const groupSiblings = this.db.select().from(runs).where(and17(
16160
+ const groupSiblings = this.db.select().from(runs).where(and18(
16016
16161
  eq19(runs.projectId, projectId),
16017
16162
  eq19(runs.kind, thisRun.kind),
16018
16163
  eq19(runs.createdAt, thisRun.createdAt),
@@ -16039,7 +16184,7 @@ var Notifier = class {
16039
16184
  );
16040
16185
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
16041
16186
  const recentRuns = this.db.select().from(runs).where(
16042
- and17(
16187
+ and18(
16043
16188
  eq19(runs.projectId, projectId),
16044
16189
  eq19(runs.kind, thisRun.kind),
16045
16190
  or(eq19(runs.status, "completed"), eq19(runs.status, "partial")),
@@ -16060,13 +16205,13 @@ var Notifier = class {
16060
16205
  provider: querySnapshots.provider,
16061
16206
  location: querySnapshots.location,
16062
16207
  citationState: querySnapshots.citationState
16063
- }).from(querySnapshots).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
16208
+ }).from(querySnapshots).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(inArray10(querySnapshots.runId, currentRunIds)).all();
16064
16209
  const previousSnapshots = this.db.select({
16065
16210
  queryId: querySnapshots.queryId,
16066
16211
  provider: querySnapshots.provider,
16067
16212
  location: querySnapshots.location,
16068
16213
  citationState: querySnapshots.citationState
16069
- }).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
16214
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
16070
16215
  const prevMap = /* @__PURE__ */ new Map();
16071
16216
  for (const s of previousSnapshots) {
16072
16217
  if (s.queryId == null) continue;
@@ -17377,6 +17522,8 @@ function mcpToAgentTool(tool, ctx) {
17377
17522
  return adapted;
17378
17523
  }
17379
17524
  var AERO_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
17525
+ // Reloading install-wide credentials remains an explicit operator action.
17526
+ CanonryMcpToolNames.canonry_providers_reload,
17380
17527
  // Aero reads stored sentiment. Configuration and backfill submission can
17381
17528
  // start paid classifier work and remain operator actions.
17382
17529
  "canonry_sentiment_configure",
@@ -17838,7 +17985,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
17838
17985
  // src/agent/runtime.ts
17839
17986
  import { z } from "zod";
17840
17987
  import { randomUUID as randomUUID3 } from "crypto";
17841
- import { isDeepStrictEqual } from "util";
17988
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
17842
17989
  import {
17843
17990
  Type as Type4,
17844
17991
  createInitialSystemMessage,
@@ -17901,7 +18048,7 @@ function resolvePageReferences(runtime, tool, params) {
17901
18048
  const page = runtime.pageReferences.get(value);
17902
18049
  if (!page) throw new Error("This Aero page reference is no longer available. Read the first page again with the original filters.");
17903
18050
  const inherited = { ...page.params, ...resolved };
17904
- if (page.tool !== tool || page.parameter !== parameter || !isDeepStrictEqual(page.selection, pageSelection(tool, inherited, parameter))) {
18051
+ if (page.tool !== tool || page.parameter !== parameter || !isDeepStrictEqual2(page.selection, pageSelection(tool, inherited, parameter))) {
17905
18052
  throw new Error("Use this Aero page reference with the same tool and original filters; only limit, queryLimit or attemptLimit may change.");
17906
18053
  }
17907
18054
  resolved = { ...inherited, [parameter]: page.token };
@@ -18663,7 +18810,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
18663
18810
 
18664
18811
  // src/agent/memory-store.ts
18665
18812
  import crypto25 from "crypto";
18666
- import { and as and18, desc as desc9, eq as eq21, like, sql as sql9, or as or2 } from "drizzle-orm";
18813
+ import { and as and19, desc as desc9, eq as eq21, like, sql as sql9, or as or2 } from "drizzle-orm";
18667
18814
  var COMPACTION_KEY_PREFIX = "compaction:";
18668
18815
  var COMPACTION_NOTES_PER_SESSION = 3;
18669
18816
  function rowToDto(row) {
@@ -18708,19 +18855,19 @@ function upsertMemoryEntry(db, args) {
18708
18855
  updatedAt: now
18709
18856
  }
18710
18857
  }).run();
18711
- const row = db.select().from(agentMemory).where(and18(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, args.key))).get();
18858
+ const row = db.select().from(agentMemory).where(and19(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, args.key))).get();
18712
18859
  if (!row) throw new Error("memory upsert produced no row");
18713
18860
  return rowToDto(row);
18714
18861
  }
18715
18862
  function deleteMemoryEntry(db, projectId, key) {
18716
- const result2 = db.delete(agentMemory).where(and18(eq21(agentMemory.projectId, projectId), eq21(agentMemory.key, key))).run();
18863
+ const result2 = db.delete(agentMemory).where(and19(eq21(agentMemory.projectId, projectId), eq21(agentMemory.key, key))).run();
18717
18864
  const changes = result2.changes ?? 0;
18718
18865
  return changes > 0;
18719
18866
  }
18720
18867
  function loadRecentForHydrate(db, projectId, limit, sessionId) {
18721
18868
  if (sessionId === void 0) return listMemoryEntries(db, projectId, { limit });
18722
18869
  const sessionPrefix = `compaction:${sessionId ?? ""}:`;
18723
- return db.select().from(agentMemory).where(and18(
18870
+ return db.select().from(agentMemory).where(and19(
18724
18871
  eq21(agentMemory.projectId, projectId),
18725
18872
  or2(sql9`substr(${agentMemory.key}, 1, ${COMPACTION_KEY_PREFIX.length}) <> ${COMPACTION_KEY_PREFIX}`, sessionId ? sql9`substr(${agentMemory.key}, 1, ${sessionPrefix.length}) = ${sessionPrefix}` : void 0)
18726
18873
  )).orderBy(desc9(agentMemory.updatedAt)).limit(limit).all().map(rowToDto);
@@ -18747,7 +18894,7 @@ function writeCompactionNote(db, args) {
18747
18894
  }).run();
18748
18895
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
18749
18896
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
18750
- and18(
18897
+ and19(
18751
18898
  eq21(agentMemory.projectId, args.projectId),
18752
18899
  like(agentMemory.key, `${sessionPrefix}%`)
18753
18900
  )
@@ -18756,7 +18903,7 @@ function writeCompactionNote(db, args) {
18756
18903
  if (stale.length > 0) {
18757
18904
  tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
18758
18905
  }
18759
- const row = tx.select().from(agentMemory).where(and18(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, key))).get();
18906
+ const row = tx.select().from(agentMemory).where(and19(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, key))).get();
18760
18907
  if (row) inserted = rowToDto(row);
18761
18908
  });
18762
18909
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -18764,7 +18911,7 @@ function writeCompactionNote(db, args) {
18764
18911
  }
18765
18912
 
18766
18913
  // src/agent/project-shape.ts
18767
- import { and as and19, count as count2, eq as eq22 } from "drizzle-orm";
18914
+ import { and as and20, count as count2, eq as eq22 } from "drizzle-orm";
18768
18915
  var ADVANCED_TOOLS = [
18769
18916
  "canonry_measurement_portfolio_summary",
18770
18917
  "canonry_measurement_overview",
@@ -18792,7 +18939,7 @@ var ADVANCED_ROUTES = [
18792
18939
  function aeroProjectShape(db, projectId) {
18793
18940
  try {
18794
18941
  const pointer = db.select().from(measurementPlans).where(eq22(measurementPlans.projectId, projectId)).get();
18795
- const version = pointer ? db.select().from(measurementPlanVersions).where(and19(
18942
+ const version = pointer ? db.select().from(measurementPlanVersions).where(and20(
18796
18943
  eq22(measurementPlanVersions.projectId, projectId),
18797
18944
  eq22(measurementPlanVersions.id, pointer.activeVersionId)
18798
18945
  )).get() : void 0;
@@ -19586,7 +19733,7 @@ ${lines.join("\n")}
19586
19733
 
19587
19734
  // src/agent/viewer-sessions.ts
19588
19735
  import crypto27 from "crypto";
19589
- import { and as and20, eq as eq24, isNotNull as isNotNull3, like as like2 } from "drizzle-orm";
19736
+ import { and as and21, eq as eq24, isNotNull as isNotNull3, like as like2 } from "drizzle-orm";
19590
19737
  var AERO_VIEWER_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
19591
19738
  CanonryMcpToolNames.canonry_ads_account,
19592
19739
  CanonryMcpToolNames.canonry_ads_geo_search,
@@ -19780,7 +19927,7 @@ var ViewerAeroSessions = class {
19780
19927
  */
19781
19928
  revokeOrphanedKeys() {
19782
19929
  try {
19783
- this.opts.db.delete(apiKeys).where(and20(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull3(apiKeys.delegatedUserId))).run();
19930
+ this.opts.db.delete(apiKeys).where(and21(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull3(apiKeys.delegatedUserId))).run();
19784
19931
  } catch {
19785
19932
  }
19786
19933
  }
@@ -21147,20 +21294,20 @@ function clipText(value, length) {
21147
21294
  }
21148
21295
 
21149
21296
  // src/research-runner.ts
21150
- import { and as and21, eq as eq27, inArray as inArray10, sql as sql10 } from "drizzle-orm";
21297
+ import { and as and22, eq as eq27, inArray as inArray11, sql as sql10 } from "drizzle-orm";
21151
21298
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
21152
21299
  function finalResearchRunStatus(completed, failed) {
21153
21300
  if (failed === 0) return ResearchRunStatuses.completed;
21154
21301
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
21155
21302
  }
21156
21303
  async function executeResearchRun(db, registry, runId, projectId) {
21157
- const run = db.select().from(researchRuns).where(and21(eq27(researchRuns.id, runId), eq27(researchRuns.projectId, projectId))).get();
21304
+ const run = db.select().from(researchRuns).where(and22(eq27(researchRuns.id, runId), eq27(researchRuns.projectId, projectId))).get();
21158
21305
  if (!run || run.status !== ResearchRunStatuses.queued) return;
21159
21306
  const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
21160
21307
  if (!project) return;
21161
21308
  const provider = registry.get(run.provider);
21162
21309
  const now = (/* @__PURE__ */ new Date()).toISOString();
21163
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and21(eq27(researchRuns.id, runId), eq27(researchRuns.status, ResearchRunStatuses.queued))).run();
21310
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and22(eq27(researchRuns.id, runId), eq27(researchRuns.status, ResearchRunStatuses.queued))).run();
21164
21311
  if (claim.changes !== 1) return;
21165
21312
  let reserved = 0;
21166
21313
  let dispatched = 0;
@@ -21195,7 +21342,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
21195
21342
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
21196
21343
  try {
21197
21344
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21198
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and21(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
21345
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and22(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
21199
21346
  const raw = await gate.run(async () => {
21200
21347
  dispatched++;
21201
21348
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -21223,7 +21370,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
21223
21370
  citationState: determineCitationState(normalized, domains),
21224
21371
  rawResponse: raw.rawResponse,
21225
21372
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
21226
- }).where(and21(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.running))).run();
21373
+ }).where(and22(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.running))).run();
21227
21374
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
21228
21375
  } catch (error) {
21229
21376
  try {
@@ -21251,11 +21398,11 @@ function incrementResearchProgress(db, runId, column) {
21251
21398
  }
21252
21399
  function markResearchQueryFailed(db, runId, queryId, error) {
21253
21400
  const message = describeError(error);
21254
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and21(eq27(researchRunQueries.id, queryId), inArray10(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
21401
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and22(eq27(researchRunQueries.id, queryId), inArray11(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
21255
21402
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
21256
21403
  }
21257
21404
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
21258
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and21(eq27(researchRunQueries.researchRunId, runId), inArray10(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
21405
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and22(eq27(researchRunQueries.researchRunId, runId), inArray11(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
21259
21406
  }
21260
21407
  function finalizeResearchRun(db, runId, fatalError) {
21261
21408
  const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq27(researchRunQueries.researchRunId, runId)).all();
@@ -21271,7 +21418,7 @@ function finalizeResearchRun(db, runId, fatalError) {
21271
21418
  }
21272
21419
 
21273
21420
  // src/google-marketing-sync.ts
21274
- import { and as and22, eq as eq28, isNull as isNull3 } from "drizzle-orm";
21421
+ import { and as and23, eq as eq28, isNull as isNull3 } from "drizzle-orm";
21275
21422
  function safeErrorMessage(error) {
21276
21423
  const raw = describeError(error);
21277
21424
  return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
@@ -21294,7 +21441,7 @@ function assertSnapshotContext(metadata2, expected) {
21294
21441
  }
21295
21442
  }
21296
21443
  function requireSyncRun(db, runId, projectId, kind) {
21297
- const run = db.select().from(runs).where(and22(
21444
+ const run = db.select().from(runs).where(and23(
21298
21445
  eq28(runs.id, runId),
21299
21446
  eq28(runs.projectId, projectId),
21300
21447
  eq28(runs.kind, kind)
@@ -21310,7 +21457,7 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
21310
21457
  status: RunStatuses.failed,
21311
21458
  error: serializeRunError({ message: safeErrorMessage(error) }),
21312
21459
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
21313
- }).where(and22(
21460
+ }).where(and23(
21314
21461
  eq28(runs.id, runId),
21315
21462
  eq28(runs.projectId, projectId),
21316
21463
  eq28(runs.kind, kind),
@@ -21318,7 +21465,7 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
21318
21465
  )).run();
21319
21466
  }
21320
21467
  function claimSyncRun(db, runId, projectId, kind, startedAt) {
21321
- const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and22(
21468
+ const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and23(
21322
21469
  eq28(runs.id, runId),
21323
21470
  eq28(runs.projectId, projectId),
21324
21471
  eq28(runs.kind, kind),
@@ -21383,7 +21530,7 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
21383
21530
  status: RunStatuses.completed,
21384
21531
  finishedAt,
21385
21532
  error: null
21386
- }).where(and22(
21533
+ }).where(and23(
21387
21534
  eq28(runs.id, runId),
21388
21535
  eq28(runs.projectId, projectId),
21389
21536
  eq28(runs.kind, RunKinds["google-ads-sync"]),
@@ -21404,7 +21551,7 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
21404
21551
  lastMetricsSnapshotAt: result2.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
21405
21552
  lastMetricsSnapshotId: result2.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
21406
21553
  updatedAt: finishedAt
21407
- }).where(and22(
21554
+ }).where(and23(
21408
21555
  eq28(googleAdsConnections.id, connection.id),
21409
21556
  eq28(googleAdsConnections.projectId, projectId),
21410
21557
  eq28(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
@@ -21473,7 +21620,7 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
21473
21620
  status: RunStatuses.completed,
21474
21621
  finishedAt,
21475
21622
  error: null
21476
- }).where(and22(
21623
+ }).where(and23(
21477
21624
  eq28(runs.id, runId),
21478
21625
  eq28(runs.projectId, projectId),
21479
21626
  eq28(runs.kind, RunKinds["gtm-sync"]),
@@ -21491,7 +21638,7 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
21491
21638
  lastSnapshotAt: snapshot.metadata.capturedAt,
21492
21639
  lastSnapshotId: snapshot.metadata.id,
21493
21640
  updatedAt: finishedAt
21494
- }).where(and22(
21641
+ }).where(and23(
21495
21642
  eq28(gtmConnections.id, connection.id),
21496
21643
  eq28(gtmConnections.projectId, projectId),
21497
21644
  eq28(gtmConnections.selectedAccountId, connection.selectedAccountId),
@@ -21530,11 +21677,7 @@ var keylessFirstRunSetupByServer = /* @__PURE__ */ new WeakMap();
21530
21677
  function allowsKeylessFirstRunSetup(app) {
21531
21678
  return keylessFirstRunSetupByServer.get(app) ?? false;
21532
21679
  }
21533
- var DEFAULT_QUOTA = {
21534
- maxConcurrency: 2,
21535
- maxRequestsPerMinute: 10,
21536
- maxRequestsPerDay: 1e3
21537
- };
21680
+ var DEFAULT_QUOTA = DEFAULT_PROVIDER_QUOTA;
21538
21681
  var SESSION_COOKIE_NAME = "canonry_session";
21539
21682
  var SESSION_TTL_MS = 12 * 60 * 60 * 1e3;
21540
21683
  function effectiveProviderModels(registry) {
@@ -21609,19 +21752,47 @@ var BROWSER_ADAPTERS = [cdpChatgptAdapter];
21609
21752
  var adapterMap = Object.fromEntries(
21610
21753
  API_ADAPTERS.map((a) => [a.name, a])
21611
21754
  );
21612
- function summarizeProviderConfig(config) {
21755
+ function summarizeProviderConfig(provider) {
21756
+ const config = provider?.config;
21613
21757
  return {
21614
- configured: Boolean(config?.apiKey || config?.baseUrl),
21758
+ configured: Boolean(config?.apiKey || config?.baseUrl || config?.vertexProject || config?.cdpEndpoint),
21615
21759
  model: config?.model ?? null,
21616
21760
  // baseUrl is surfaced for ALL providers, not just local — gemini/openai now
21617
21761
  // honor a custom endpoint, so repointing one must show in the settings
21618
21762
  // summary AND produce an audit diff. Omitting it for API providers would let
21619
21763
  // an endpoint redirect (a credential-exfiltration vector on a box where the
21620
21764
  // provider key is the carrier) happen with no audit trail.
21621
- baseUrl: config?.baseUrl ?? null,
21622
- quota: { ...config?.quota ?? DEFAULT_QUOTA }
21765
+ baseUrl: config?.baseUrl ?? config?.cdpEndpoint ?? null,
21766
+ quota: { ...config?.quotaPolicy ?? DEFAULT_QUOTA },
21767
+ vertexProject: config?.vertexProject ?? null,
21768
+ vertexRegion: config?.vertexRegion ?? null,
21769
+ batch: config?.batch ?? null,
21770
+ pricing: config?.pricing ?? null
21623
21771
  };
21624
21772
  }
21773
+ function providerSummaries(registry, config, includeBrowser = false) {
21774
+ return [...API_ADAPTERS, ...includeBrowser ? BROWSER_ADAPTERS : []].map((adapter) => ({
21775
+ name: adapter.name,
21776
+ displayName: adapter.displayName,
21777
+ keyUrl: adapter.keyUrl,
21778
+ modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
21779
+ model: registry.get(adapter.name)?.config.model ?? void 0,
21780
+ defaultModel: adapter.modelRegistry.defaultModel,
21781
+ configured: Boolean(registry.get(adapter.name)),
21782
+ quota: registry.get(adapter.name)?.config.quotaPolicy,
21783
+ vertexConfigured: adapter.name === ProviderNames.gemini ? Boolean(config.providers?.gemini?.vertexProject) : void 0
21784
+ }));
21785
+ }
21786
+ function runtimePathIdentity(runtimePath) {
21787
+ if (runtimePath === ":memory:") return runtimePath;
21788
+ const resolved = path8.resolve(runtimePath);
21789
+ let directory = path8.dirname(resolved);
21790
+ try {
21791
+ directory = fs9.realpathSync(directory);
21792
+ } catch {
21793
+ }
21794
+ return path8.join(directory, path8.basename(resolved));
21795
+ }
21625
21796
  var DASHBOARD_SCRYPT_KEYLEN = 64;
21626
21797
  var DASHBOARD_SCRYPT_COST = 1 << 15;
21627
21798
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
@@ -21932,6 +22103,10 @@ function createGoogleMarketingConfigCredentialStore(input) {
21932
22103
  };
21933
22104
  }
21934
22105
  async function createServer(opts) {
22106
+ const runtimeConfigPath = path8.resolve(getConfigPath());
22107
+ const runtimeConfigIdentity = runtimePathIdentity(runtimeConfigPath);
22108
+ const runtimeDatabasePath = opts.config.database;
22109
+ const runtimeDatabaseIdentity = runtimePathIdentity(runtimeDatabasePath);
21935
22110
  const embed = resolveEmbedConfig(process.env, opts.config);
21936
22111
  const operatorApiKeyIds = resolveOperatorApiKeyIds(process.env);
21937
22112
  const dashboardManagedRunKinds = resolveDashboardManagedRunKinds(process.env, opts.config);
@@ -21993,6 +22168,7 @@ async function createServer(opts) {
21993
22168
  quota: opts.config.geminiQuota
21994
22169
  };
21995
22170
  }
22171
+ opts.config.providers = providers;
21996
22172
  try {
21997
22173
  const legacyRows = extractLegacyCredentials(opts.db);
21998
22174
  applyLegacyCredentials(legacyRows, opts.config);
@@ -22003,39 +22179,15 @@ async function createServer(opts) {
22003
22179
  });
22004
22180
  }
22005
22181
  log25.info("providers.configured", {
22006
- providers: Object.keys(providers).filter((k) => {
22007
- const p = providers[k];
22008
- return p?.apiKey || p?.baseUrl || p?.vertexProject;
22009
- })
22182
+ providers: registeredProviderNames({ providers, cdp: opts.config.cdp })
22010
22183
  });
22011
- for (const adapter of API_ADAPTERS) {
22012
- const entry = providers[adapter.name];
22013
- if (!entry) continue;
22014
- const isConfigured = adapter.name === "local" ? !!entry.baseUrl : adapter.name === "gemini" ? !!(entry.apiKey || entry.vertexProject) : !!entry.apiKey;
22015
- if (isConfigured) {
22016
- registry.register(adapter, providerConfigFromEntry(adapter.name, entry, entry.quota ?? DEFAULT_QUOTA));
22017
- }
22018
- }
22184
+ registry.replace(configuredProviderEntries(opts.config, API_ADAPTERS, cdpChatgptAdapter));
22019
22185
  for (const name of providersWithUnsupportedBatch(providers, (provider) => adapterMap[provider])) {
22020
22186
  log25.warn("provider.batch.unsupported", {
22021
22187
  provider: name,
22022
22188
  message: `providers.${name}.batch.enabled is true, but the ${name} adapter has no batch API; its sweeps run sync.`
22023
22189
  });
22024
22190
  }
22025
- const cdpConfig = opts.config.cdp;
22026
- if (cdpConfig?.host || cdpConfig?.port) {
22027
- const CDP_DEFAULT_QUOTA = {
22028
- maxConcurrency: 1,
22029
- maxRequestsPerMinute: 4,
22030
- maxRequestsPerDay: 200
22031
- };
22032
- const cdpEndpoint = `ws://${cdpConfig.host ?? "localhost"}:${cdpConfig.port ?? 9222}`;
22033
- registry.register(cdpChatgptAdapter, {
22034
- provider: "cdp:chatgpt",
22035
- cdpEndpoint,
22036
- quotaPolicy: cdpConfig.quota ?? CDP_DEFAULT_QUOTA
22037
- });
22038
- }
22039
22191
  const port = opts.config.port ?? 4100;
22040
22192
  const serverUrl = `http://localhost:${port}`;
22041
22193
  const jobRunner = new JobRunner(opts.db, registry, {
@@ -22566,7 +22718,7 @@ async function createServer(opts) {
22566
22718
  });
22567
22719
  if (!probed) return;
22568
22720
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
22569
- and23(
22721
+ and24(
22570
22722
  eq29(ccReleaseSyncs.release, probed.release),
22571
22723
  eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
22572
22724
  )
@@ -22594,17 +22746,7 @@ async function createServer(opts) {
22594
22746
  runSiteAudit(runId, projectId, auditOpts);
22595
22747
  }
22596
22748
  });
22597
- const providerSummary = API_ADAPTERS.map((adapter) => ({
22598
- name: adapter.name,
22599
- displayName: adapter.displayName,
22600
- keyUrl: adapter.keyUrl,
22601
- modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
22602
- model: registry.get(adapter.name)?.config.model,
22603
- defaultModel: adapter.modelRegistry.defaultModel,
22604
- configured: !!registry.get(adapter.name),
22605
- quota: registry.get(adapter.name)?.config.quotaPolicy,
22606
- vertexConfigured: adapter.name === "gemini" ? !!opts.config.providers?.gemini?.vertexProject : void 0
22607
- }));
22749
+ const providerSummary = providerSummaries(registry, opts.config);
22608
22750
  const googleSettingsSummary = {
22609
22751
  configured: Boolean(
22610
22752
  opts.config.google?.clientId && opts.config.google?.clientSecret
@@ -22858,7 +23000,7 @@ async function createServer(opts) {
22858
23000
  const currentPasswordFingerprint = () => hashSessionToken(opts.config.dashboardPasswordHash ?? "");
22859
23001
  opts.db.delete(dashboardSessions).where(or3(
22860
23002
  eq29(dashboardSessions.apiKeyHash, ""),
22861
- and23(
23003
+ and24(
22862
23004
  isNotNull4(dashboardSessions.passwordFingerprint),
22863
23005
  ne3(dashboardSessions.passwordFingerprint, currentPasswordFingerprint())
22864
23006
  )
@@ -22885,7 +23027,7 @@ async function createServer(opts) {
22885
23027
  currentApiKeyHash: apiKeys.keyHash,
22886
23028
  passwordFingerprint: dashboardSessions.passwordFingerprint,
22887
23029
  expiresAt: dashboardSessions.expiresAt
22888
- }).from(dashboardSessions).innerJoin(apiKeys, eq29(apiKeys.id, dashboardSessions.apiKeyId)).where(and23(eq29(dashboardSessions.tokenHash, tokenHash), isNull4(apiKeys.revokedAt))).get();
23030
+ }).from(dashboardSessions).innerJoin(apiKeys, eq29(apiKeys.id, dashboardSessions.apiKeyId)).where(and24(eq29(dashboardSessions.tokenHash, tokenHash), isNull4(apiKeys.revokedAt))).get();
22889
23031
  if (!session) return null;
22890
23032
  const expiresAt = Date.parse(session.expiresAt);
22891
23033
  if (!Number.isFinite(expiresAt) || expiresAt <= Date.now() || session.apiKeyHash !== session.currentApiKeyHash || session.passwordFingerprint !== null && session.passwordFingerprint !== currentPasswordFingerprint()) {
@@ -23148,7 +23290,7 @@ async function createServer(opts) {
23148
23290
  opts.db.update(projects).set({ answerFieldsRecompute: null }).where(eq29(projects.id, projectId)).run();
23149
23291
  if (recomputeDebts.get(projectId) === debt) recomputeDebts.delete(projectId);
23150
23292
  } else if (debt.latestFull === 0) {
23151
- opts.db.update(projects).set({ answerFieldsRecompute: "competitors" }).where(and23(eq29(projects.id, projectId), eq29(projects.answerFieldsRecompute, "full"))).run();
23293
+ opts.db.update(projects).set({ answerFieldsRecompute: "competitors" }).where(and24(eq29(projects.id, projectId), eq29(projects.answerFieldsRecompute, "full"))).run();
23152
23294
  }
23153
23295
  } catch (err) {
23154
23296
  app.log.error({ err, projectId }, "could not clear the owed answer field recompute");
@@ -23238,6 +23380,34 @@ async function createServer(opts) {
23238
23380
  await sentimentPoller.settled();
23239
23381
  });
23240
23382
  const providerModelCatalog = createProviderModelCatalog(registry);
23383
+ const recordProviderChange = (name, before, after, auditContext) => {
23384
+ const beforeConfig = summarizeProviderConfig(before);
23385
+ const afterConfig = summarizeProviderConfig(after);
23386
+ const apiKeyRotated = Boolean(before) && Boolean(after?.config.apiKey) && before?.config.apiKey !== after?.config.apiKey;
23387
+ const credentialsChanged = before?.config.vertexCredentials !== after?.config.vertexCredentials;
23388
+ if (!apiKeyRotated && !credentialsChanged && JSON.stringify(beforeConfig) === JSON.stringify(afterConfig)) return;
23389
+ const affectedProjects = opts.db.select({ id: projects.id, providers: projects.providers }).from(projects).all().filter((project) => project.providers.length === 0 || project.providers.includes(name));
23390
+ const projectIds = affectedProjects.length ? affectedProjects.map((project) => project.id) : [null];
23391
+ for (const projectId of projectIds) {
23392
+ writeAuditLog(opts.db, {
23393
+ projectId,
23394
+ actor: auditContext?.actor ?? "api",
23395
+ userAgent: auditContext?.userAgent,
23396
+ actorSession: auditContext?.actorSession,
23397
+ credentialId: auditContext?.credentialId,
23398
+ requestId: auditContext?.requestId,
23399
+ action: after ? before ? "provider.updated" : "provider.created" : "provider.removed",
23400
+ entityType: "provider",
23401
+ entityId: name,
23402
+ diff: {
23403
+ before: before ? beforeConfig : null,
23404
+ after: afterConfig,
23405
+ ...apiKeyRotated ? { apiKeyRotated: true } : {},
23406
+ ...credentialsChanged ? { credentialsChanged: true } : {}
23407
+ }
23408
+ });
23409
+ }
23410
+ };
23241
23411
  await app.register(apiRoutes, {
23242
23412
  db: opts.db,
23243
23413
  sentiment: {
@@ -23288,13 +23458,10 @@ async function createServer(opts) {
23288
23458
  // and would have thrown if the file were missing; tests that construct
23289
23459
  // `createServer` directly (bypassing `loadConfig`) won't have written
23290
23460
  // a config and shouldn't get 503s from a stub-missing file.
23291
- runtimeStatePaths: (() => {
23292
- const configPath = getConfigPath();
23293
- return {
23294
- databasePath: opts.config.database,
23295
- configPath: fs9.existsSync(configPath) ? configPath : null
23296
- };
23297
- })(),
23461
+ runtimeStatePaths: {
23462
+ databasePath: runtimeDatabasePath,
23463
+ configPath: fs9.existsSync(runtimeConfigPath) ? runtimeConfigPath : null
23464
+ },
23298
23465
  // Snapshot the bundled skill trees (version + file hashes) so the
23299
23466
  // `agent.skills.current` doctor check can flag a `~/.claude/skills/` install
23300
23467
  // that has drifted behind this build. Best-effort: if the bundled assets
@@ -23657,12 +23824,43 @@ async function createServer(opts) {
23657
23824
  getRunnableProviderNames: () => registry.getAll().map((provider) => provider.adapter.name),
23658
23825
  getEffectiveProviderModels: () => effectiveProviderModels(registry),
23659
23826
  getBatchEligibleProviderNames: () => batchEligibleProviderNames(registry),
23827
+ onProviderReload: (input, auditContext) => {
23828
+ if (input.configPath !== void 0 && (runtimePathIdentity(input.configPath) !== runtimeConfigIdentity || runtimePathIdentity(input.databasePath) !== runtimeDatabaseIdentity)) {
23829
+ throw validationError("Provider reload identity does not match this running server.", {
23830
+ reason: ProviderReloadErrorReasons["install-identity-mismatch"]
23831
+ });
23832
+ }
23833
+ let loaded;
23834
+ try {
23835
+ loaded = loadConfig(runtimeConfigPath);
23836
+ } catch {
23837
+ throw validationError("Provider configuration could not be loaded; repair config.yaml and retry.");
23838
+ }
23839
+ const next = configuredProviderEntries(loaded, API_ADAPTERS, cdpChatgptAdapter);
23840
+ assertProviderReloadKeepsPendingBatches(opts.db, registry.getAll(), next, jobRunner.getExecutingBatchRegistrations());
23841
+ const nextByName = new Map(next.map((provider) => [provider.adapter.name, provider]));
23842
+ const names = /* @__PURE__ */ new Set([...registry.getAll().map((provider) => provider.adapter.name), ...nextByName.keys()]);
23843
+ opts.db.transaction(() => {
23844
+ for (const name of names) recordProviderChange(name, registry.get(name), nextByName.get(name), auditContext);
23845
+ });
23846
+ registry.replace(next);
23847
+ opts.config.providers = loaded.providers ?? {};
23848
+ opts.config.cdp = loaded.cdp;
23849
+ opts.config.geminiApiKey = loaded.geminiApiKey;
23850
+ opts.config.geminiModel = loaded.geminiModel;
23851
+ opts.config.geminiQuota = loaded.geminiQuota;
23852
+ providerSummary.splice(0, providerSummary.length, ...providerSummaries(registry, opts.config));
23853
+ return providerSummaries(registry, opts.config, true);
23854
+ },
23660
23855
  onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota, auditContext) => {
23661
23856
  const name = providerName;
23662
23857
  if (!adapterMap[name]) return null;
23663
23858
  if (!opts.config.providers) opts.config.providers = {};
23664
23859
  const existing = opts.config.providers[name];
23665
- const beforeConfig = summarizeProviderConfig(existing);
23860
+ const previousProvider = registry.get(name) ?? (existing ? {
23861
+ adapter: adapterMap[name],
23862
+ config: providerConfigFromEntry(name, existing, resolveProviderQuotaPolicy(existing.quota, DEFAULT_QUOTA))
23863
+ } : void 0);
23666
23864
  const mergedQuota = incomingQuota ? { ...existing?.quota ?? DEFAULT_QUOTA, ...incomingQuota } : existing?.quota;
23667
23865
  opts.config.providers[name] = {
23668
23866
  apiKey: apiKey || existing?.apiKey,
@@ -23680,53 +23878,16 @@ async function createServer(opts) {
23680
23878
  ...existing?.pricing ? { pricing: existing.pricing } : {}
23681
23879
  };
23682
23880
  try {
23683
- saveConfigPatch(opts.config);
23881
+ saveConfigPatch(fs9.existsSync(runtimeConfigPath) ? { providers: { [name]: opts.config.providers[name] } } : opts.config);
23684
23882
  } catch (err) {
23685
23883
  app.log.error({ err }, "Failed to save config");
23686
23884
  return null;
23687
23885
  }
23688
- const quota = opts.config.providers[name].quota ?? DEFAULT_QUOTA;
23886
+ const quota = resolveProviderQuotaPolicy(opts.config.providers[name].quota, DEFAULT_QUOTA);
23689
23887
  registry.register(adapterMap[name], providerConfigFromEntry(name, opts.config.providers[name], quota));
23690
- const entry = providerSummary.find((p) => p.name === name);
23691
- if (entry) {
23692
- entry.configured = true;
23693
- entry.model = registry.get(name)?.config.model;
23694
- entry.quota = quota;
23695
- if (name === "gemini") {
23696
- entry.vertexConfigured = !!opts.config.providers?.[name]?.vertexProject;
23697
- }
23698
- }
23699
- const afterConfig = summarizeProviderConfig(opts.config.providers[name]);
23700
- const apiKeyRotated = Boolean(existing) && Boolean(apiKey) && apiKey !== existing?.apiKey;
23701
- if (apiKeyRotated || JSON.stringify(beforeConfig) !== JSON.stringify(afterConfig)) {
23702
- const diff = JSON.stringify({
23703
- before: existing ? beforeConfig : null,
23704
- after: afterConfig,
23705
- ...apiKeyRotated ? { apiKeyRotated: true } : {}
23706
- });
23707
- const affectedProjectIds = opts.db.select({ id: projects.id, providers: projects.providers }).from(projects).all().filter((project) => {
23708
- const configuredProviders = project.providers;
23709
- return configuredProviders.length === 0 || configuredProviders.includes(name);
23710
- }).map((project) => project.id);
23711
- const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
23712
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
23713
- opts.db.insert(auditLog).values(
23714
- targetProjectIds.map((projectId) => ({
23715
- id: crypto29.randomUUID(),
23716
- projectId,
23717
- actor: auditContext?.actor ?? "api",
23718
- userAgent: auditContext?.userAgent ?? null,
23719
- actorSession: auditContext?.actorSession ?? null,
23720
- credentialId: auditContext?.credentialId ?? null,
23721
- requestId: auditContext?.requestId ?? null,
23722
- action: existing ? "provider.updated" : "provider.created",
23723
- entityType: "provider",
23724
- entityId: name,
23725
- diff,
23726
- createdAt
23727
- }))
23728
- ).run();
23729
- }
23888
+ providerSummary.splice(0, providerSummary.length, ...providerSummaries(registry, opts.config));
23889
+ const entry = providerSummary.find((provider) => provider.name === name);
23890
+ opts.db.transaction(() => recordProviderChange(name, previousProvider, registry.get(name), auditContext));
23730
23891
  return {
23731
23892
  name,
23732
23893
  model: entry?.model,
@@ -23813,15 +23974,10 @@ async function createServer(opts) {
23813
23974
  app.log.error({ err }, "Failed to save CDP config");
23814
23975
  throw err;
23815
23976
  }
23816
- const CDP_DEFAULT_QUOTA = {
23817
- maxConcurrency: 1,
23818
- maxRequestsPerMinute: 4,
23819
- maxRequestsPerDay: 200
23820
- };
23821
23977
  registry.register(cdpChatgptAdapter, {
23822
23978
  provider: "cdp:chatgpt",
23823
23979
  cdpEndpoint: `ws://${host}:${port2}`,
23824
- quotaPolicy: opts.config.cdp.quota ?? CDP_DEFAULT_QUOTA
23980
+ quotaPolicy: resolveProviderQuotaPolicy(opts.config.cdp.quota, DEFAULT_CDP_QUOTA)
23825
23981
  });
23826
23982
  },
23827
23983
  getCdpStatus: async () => {
@@ -24168,6 +24324,7 @@ export {
24168
24324
  detectAndTrackUpgrade,
24169
24325
  trackEvent,
24170
24326
  setTelemetryPreference,
24327
+ registeredProviderNames,
24171
24328
  OPENAI_RETRIEVAL_CONTRACT,
24172
24329
  reparseStoredResult2 as reparseStoredResult,
24173
24330
  reparseStoredResult3 as reparseStoredResult2,