@ainyc/canonry 5.2.1 → 5.3.1

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/README.md +2 -2
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -0
  3. package/assets/assets/{AuditHistoryPanel-CkBLHz2H.js → AuditHistoryPanel-D5cOFMKE.js} +1 -1
  4. package/assets/assets/{BacklinksPage-D2YWJUf9.js → BacklinksPage-BiOj8ztV.js} +1 -1
  5. package/assets/assets/{HistoryPage-C9j3RzMN.js → HistoryPage-DDBCwooU.js} +1 -1
  6. package/assets/assets/MeasurementPropertyPage-CMoU2yA_.js +1 -0
  7. package/assets/assets/ProjectPage-CrFybBls.js +8 -0
  8. package/assets/assets/{RunRow-DTJuJ8mb.js → RunRow-BLBAwIot.js} +1 -1
  9. package/assets/assets/{RunsPage-CdKorpOj.js → RunsPage-BVgBbE1M.js} +1 -1
  10. package/assets/assets/{SettingsPage-BOjZh2vW.js → SettingsPage-DYtN-1_-.js} +1 -1
  11. package/assets/assets/{SiteHealthSection-D7sPztpW.js → SiteHealthSection-ElUwcAS4.js} +3 -3
  12. package/assets/assets/{TrafficPage-BCodKdgN.js → TrafficPage-D6QT_2hU.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-D_zueOK5.js → TrafficSourceDetailPage-BnLzF82w.js} +1 -1
  14. package/assets/assets/{extract-error-message-BZYGBapN.js → extract-error-message-DIzjGZxl.js} +1 -1
  15. package/assets/assets/{index-aZ69fB-u.js → index-CoQVOsWw.js} +3 -3
  16. package/assets/assets/{react-sigma_core.esm.min-m72qsd73.js → react-sigma_core.esm.min-DTbIcdn4.js} +1 -1
  17. package/assets/assets/v2-overview-adapter-B_N4ilQV.js +1 -0
  18. package/assets/assets/{vendor-lucide-D_WBzb76.js → vendor-lucide-nVpYViBH.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-5VONCLVQ.js → chunk-3COTRCK3.js} +1 -1
  21. package/dist/{chunk-YQNKULAD.js → chunk-IACO5R7A.js} +837 -367
  22. package/dist/{chunk-AGUCTX7L.js → chunk-TF6HFT3Q.js} +198 -57
  23. package/dist/cli.js +6 -6
  24. package/dist/{demo-server-BIPT53EY.js → demo-server-TXW3UX3P.js} +2 -2
  25. package/dist/index.js +3 -3
  26. package/dist/{intelligence-service-SJO5E7WN.js → intelligence-service-H34EOB6K.js} +1 -1
  27. package/package.json +13 -13
  28. package/assets/assets/MeasurementPropertyPage-Ci3CUr7Q.js +0 -1
  29. package/assets/assets/ProjectPage-Bdq_KTXX.js +0 -8
  30. package/assets/assets/v2-overview-adapter-BgsdlByS.js +0 -1
@@ -21,7 +21,7 @@ import {
21
21
  deleteSiteCrawlGraphLayout,
22
22
  persistSiteCrawlGraphLayout,
23
23
  prepareSiteCrawlGraphLayout
24
- } from "./chunk-5VONCLVQ.js";
24
+ } from "./chunk-3COTRCK3.js";
25
25
  import {
26
26
  PACKAGE_VERSION
27
27
  } from "./chunk-AOJU5LKX.js";
@@ -34,6 +34,8 @@ import {
34
34
  IntelligenceService,
35
35
  OperationalLogStore,
36
36
  PLUGIN_DIR,
37
+ SITE_REACHABILITY_CHECKS,
38
+ SITE_REACHABILITY_CHECK_ID,
37
39
  USER_SESSION_COOKIE_NAME,
38
40
  addLogListener,
39
41
  adsAdGroups,
@@ -158,6 +160,7 @@ import {
158
160
  resolveUserSession,
159
161
  resolveVercelSyncDeadlineMs,
160
162
  resolveWebhookTarget,
163
+ runChecks,
161
164
  runs,
162
165
  schedules,
163
166
  serializeUserSessionCookie,
@@ -170,9 +173,10 @@ import {
170
173
  siteCrawlPages,
171
174
  siteCrawlRunRequests,
172
175
  siteCrawlSnapshots,
176
+ siteLivenessState,
173
177
  toAlertView,
174
178
  usageCounters
175
- } from "./chunk-YQNKULAD.js";
179
+ } from "./chunk-IACO5R7A.js";
176
180
  import {
177
181
  AGENT_MEMORY_VALUE_MAX_BYTES,
178
182
  AGENT_NONE,
@@ -6457,7 +6461,7 @@ function nonBlank(value) {
6457
6461
  return trimmed ? trimmed : void 0;
6458
6462
  }
6459
6463
  function resolveBuildCommit(env = process.env) {
6460
- const embedded = true ? "9406b9055f3ad102471d119cacfa568901f5d0d4" : void 0;
6464
+ const embedded = true ? "faea7a16b83966f055c85e798a03c0a4108c1377" : void 0;
6461
6465
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6462
6466
  }
6463
6467
  function resolveInstanceIdentity(env = process.env) {
@@ -11751,7 +11755,7 @@ function readStoredGroundingSources(rawResponse) {
11751
11755
  return result;
11752
11756
  }
11753
11757
  async function backfillInsightsCommand(project, opts) {
11754
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SJO5E7WN.js");
11758
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-H34EOB6K.js");
11755
11759
  const config = loadConfig();
11756
11760
  const db = createClient(config.database);
11757
11761
  migrate(db);
@@ -12901,8 +12905,48 @@ var Scheduler = class {
12901
12905
  }
12902
12906
  };
12903
12907
 
12908
+ // src/site-liveness-loop.ts
12909
+ var log15 = createLogger("SiteLiveness");
12910
+ var SITE_LIVENESS_INTERVAL_MS = 10 * 6e4;
12911
+ async function runSiteLivenessPass(deps) {
12912
+ const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
12913
+ const rows = deps.db.select({ id: projects.id, name: projects.name, canonicalDomain: projects.canonicalDomain, displayName: projects.displayName }).from(projects).all();
12914
+ let checked = 0;
12915
+ let events = 0;
12916
+ for (const project of rows) {
12917
+ try {
12918
+ const check = await deps.probe(project);
12919
+ if (!check) continue;
12920
+ checked += 1;
12921
+ const event = await deps.notify(project.id, { check, checkedAt: now() });
12922
+ if (event) {
12923
+ events += 1;
12924
+ log15.info("site-liveness.notified", { projectName: project.name, event, code: check.code });
12925
+ }
12926
+ } catch (err) {
12927
+ log15.warn("site-liveness.project-failed", { projectName: project.name, err: String(err) });
12928
+ }
12929
+ }
12930
+ return { checked, events };
12931
+ }
12932
+ function startSiteLivenessLoop(deps, intervalMs = SITE_LIVENESS_INTERVAL_MS) {
12933
+ let running = false;
12934
+ const timer = setInterval(() => {
12935
+ if (running) {
12936
+ log15.warn("site-liveness.pass-still-running", { intervalMs });
12937
+ return;
12938
+ }
12939
+ running = true;
12940
+ void runSiteLivenessPass(deps).catch((err) => log15.warn("site-liveness.pass-failed", { err: String(err) })).finally(() => {
12941
+ running = false;
12942
+ });
12943
+ }, intervalMs);
12944
+ timer.unref?.();
12945
+ return () => clearInterval(timer);
12946
+ }
12947
+
12904
12948
  // src/data-refresh.ts
12905
- var log15 = createLogger("DataRefresh");
12949
+ var log16 = createLogger("DataRefresh");
12906
12950
  async function refreshAllIntegrations(client, projectName) {
12907
12951
  const integrations = [
12908
12952
  { name: "gsc", run: () => client.gscSync(projectName, {}) },
@@ -12919,11 +12963,11 @@ async function refreshAllIntegrations(client, projectName) {
12919
12963
  results.forEach((result, idx) => {
12920
12964
  const integration = integrations[idx].name;
12921
12965
  if (result.status === "fulfilled") {
12922
- log15.info("integration.refreshed", { projectName, integration });
12966
+ log16.info("integration.refreshed", { projectName, integration });
12923
12967
  } else {
12924
12968
  const reason = result.reason;
12925
12969
  const message = describeError(reason);
12926
- log15.warn("integration.refresh-failed", { projectName, integration, error: message });
12970
+ log16.warn("integration.refresh-failed", { projectName, integration, error: message });
12927
12971
  }
12928
12972
  });
12929
12973
  }
@@ -12931,7 +12975,7 @@ async function refreshAllIntegrations(client, projectName) {
12931
12975
  // src/notifier.ts
12932
12976
  import { eq as eq17, desc as desc6, and as and15, inArray as inArray8, or } from "drizzle-orm";
12933
12977
  import crypto20 from "crypto";
12934
- var log16 = createLogger("Notifier");
12978
+ var log17 = createLogger("Notifier");
12935
12979
  var MATERIAL_MAGNITUDE_DELTA = 20;
12936
12980
  var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
12937
12981
  function severityRank(severity) {
@@ -12947,6 +12991,9 @@ function insightMagnitude(insight) {
12947
12991
  function insightNotifyKey(projectId, insight) {
12948
12992
  return `${projectId}:${insight.type}:${insight.query}:${insightFingerprint(insight)}`;
12949
12993
  }
12994
+ var SITE_LIVENESS_FAILURES_TO_PAGE = 2;
12995
+ var SITE_LIVENESS_MIN_PASS_GAP_MS = 4 * 6e4;
12996
+ var SITE_LIVENESS_MAX_PASS_GAP_MS = 45 * 6e4;
12950
12997
  var Notifier = class {
12951
12998
  db;
12952
12999
  serverUrl;
@@ -12956,26 +13003,26 @@ var Notifier = class {
12956
13003
  }
12957
13004
  /** Called after a run completes (success, partial, or failed). */
12958
13005
  async onRunCompleted(runId, projectId) {
12959
- log16.info("run.completed", { runId, projectId });
13006
+ log17.info("run.completed", { runId, projectId });
12960
13007
  const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
12961
13008
  if (notifs.length === 0) {
12962
- log16.info("notifications.none-enabled", { projectId });
13009
+ log17.info("notifications.none-enabled", { projectId });
12963
13010
  return;
12964
13011
  }
12965
- log16.info("notifications.found", { projectId, count: notifs.length });
13012
+ log17.info("notifications.found", { projectId, count: notifs.length });
12966
13013
  const run = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
12967
13014
  if (!run) {
12968
- log16.error("run.not-found", { runId, msg: "skipping notification dispatch" });
13015
+ log17.error("run.not-found", { runId, msg: "skipping notification dispatch" });
12969
13016
  return;
12970
13017
  }
12971
13018
  const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
12972
13019
  if (!project) {
12973
- log16.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
13020
+ log17.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
12974
13021
  return;
12975
13022
  }
12976
13023
  const transitions = this.computeTransitions(runId, projectId);
12977
13024
  const events = [];
12978
- log16.info("run.status", { runId: run.id, status: run.status, projectId });
13025
+ log17.info("run.status", { runId: run.id, status: run.status, projectId });
12979
13026
  if (run.status === "completed" || run.status === "partial") {
12980
13027
  events.push("run.completed");
12981
13028
  }
@@ -12991,7 +13038,7 @@ var Notifier = class {
12991
13038
  if (!config.url) continue;
12992
13039
  const subscribedEvents = config.events;
12993
13040
  const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
12994
- log16.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
13041
+ log17.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
12995
13042
  if (matchingEvents.length === 0) continue;
12996
13043
  for (const event of matchingEvents) {
12997
13044
  const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
@@ -13023,7 +13070,7 @@ var Notifier = class {
13023
13070
  async onHealthChecked(projectId, report) {
13024
13071
  const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
13025
13072
  if (!project) {
13026
- log16.error("project.not-found", { projectId, msg: "skipping health notification" });
13073
+ log17.error("project.not-found", { projectId, msg: "skipping health notification" });
13027
13074
  return null;
13028
13075
  }
13029
13076
  const rank = (status2) => status2 === "fail" ? 2 : status2 === "warn" ? 1 : 0;
@@ -13054,6 +13101,7 @@ var Notifier = class {
13054
13101
  const summary = noSignal ? `No health check produced a result (${report.checks.length} skipped) \u2014 health is unknown, not confirmed.` : worst?.summary ?? `All ${graded.length} health check(s) passing.`;
13055
13102
  const previous = this.db.select().from(doctorHealthState).where(eq17(doctorHealthState.projectId, projectId)).get();
13056
13103
  const previousStatus = previous?.status ?? null;
13104
+ const failingSignature = failing.map((c) => `${c.status}:${c.code}`).join(",");
13057
13105
  let event = null;
13058
13106
  if (previous === void 0) {
13059
13107
  if (status !== "ok") event = "health.degraded";
@@ -13061,16 +13109,18 @@ var Notifier = class {
13061
13109
  event = "health.recovered";
13062
13110
  } else if (status !== "ok" && (previousStatus === "ok" || previous.code !== code)) {
13063
13111
  event = "health.degraded";
13112
+ } else if (status !== "ok" && previous.failingSignature !== null && previous.failingSignature !== failingSignature) {
13113
+ event = "health.degraded";
13064
13114
  }
13065
13115
  const now = report.checkedAt;
13066
- const observation = { projectId, status, code, summary, checkedAt: now };
13116
+ const observation = { projectId, status, code, summary, checkedAt: now, failingSignature };
13067
13117
  if (previous === void 0) {
13068
13118
  this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
13069
13119
  } else {
13070
13120
  this.db.update(doctorHealthState).set(observation).where(eq17(doctorHealthState.projectId, projectId)).run();
13071
13121
  }
13072
13122
  if (!event) {
13073
- log16.info("health.unchanged", { projectId, status, code });
13123
+ log17.info("health.unchanged", { projectId, status, code });
13074
13124
  return null;
13075
13125
  }
13076
13126
  const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
@@ -13093,13 +13143,87 @@ var Notifier = class {
13093
13143
  for (const notif of notifs) {
13094
13144
  const config = notif.config;
13095
13145
  if (!config.url) continue;
13096
- await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
13097
- delivered += 1;
13146
+ if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
13098
13147
  }
13099
13148
  if (delivered > 0) {
13100
13149
  this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq17(doctorHealthState.projectId, projectId)).run();
13101
13150
  }
13102
- log16.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
13151
+ log17.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
13152
+ return event;
13153
+ }
13154
+ /**
13155
+ * Record a website liveness probe and notify on a confirmed change.
13156
+ *
13157
+ * Separate from onHealthChecked: that state grades the whole 6h doctor pass,
13158
+ * and this pass sees one check. Sharing its row would let "site is up" clear
13159
+ * an unrelated GA outage and send a false recovery.
13160
+ *
13161
+ * Two failed passes in a row before paging. The probe already retries once
13162
+ * inside a pass, so an alert means four failed requests spread over at least
13163
+ * one schedule interval, not a blip or a host restart. A recovery is sent only
13164
+ * for an outage that was paged, so a blip that never alerted stays silent in
13165
+ * both directions.
13166
+ */
13167
+ async onSiteLivenessChecked(projectId, result) {
13168
+ const { check, checkedAt } = result;
13169
+ if (check.status !== "ok" && check.status !== "fail") return null;
13170
+ const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
13171
+ if (!project) {
13172
+ log17.error("project.not-found", { projectId, msg: "skipping site liveness notification" });
13173
+ return null;
13174
+ }
13175
+ const previous = this.db.select().from(siteLivenessState).where(eq17(siteLivenessState.projectId, projectId)).get();
13176
+ const sincePrevious = previous ? Date.parse(checkedAt) - Date.parse(previous.checkedAt) : Number.NaN;
13177
+ const countsAsRepeat = Number.isFinite(sincePrevious) && sincePrevious >= SITE_LIVENESS_MIN_PASS_GAP_MS && sincePrevious <= SITE_LIVENESS_MAX_PASS_GAP_MS;
13178
+ const failures = check.status === "fail" ? countsAsRepeat ? (previous?.consecutiveFailures ?? 0) + 1 : 1 : 0;
13179
+ const outagePaged = previous?.status === "fail" && Boolean(previous.notifiedAt);
13180
+ let event = null;
13181
+ if (check.status === "fail" && failures >= SITE_LIVENESS_FAILURES_TO_PAGE && !outagePaged) event = "health.degraded";
13182
+ if (check.status === "ok" && outagePaged) event = "health.recovered";
13183
+ const observation = {
13184
+ projectId,
13185
+ status: check.status,
13186
+ code: check.code,
13187
+ summary: check.summary,
13188
+ consecutiveFailures: failures,
13189
+ checkedAt,
13190
+ notifiedAt: check.status === "ok" ? null : previous?.notifiedAt ?? null
13191
+ };
13192
+ if (previous === void 0) {
13193
+ this.db.insert(siteLivenessState).values(observation).run();
13194
+ } else {
13195
+ this.db.update(siteLivenessState).set(observation).where(eq17(siteLivenessState.projectId, projectId)).run();
13196
+ }
13197
+ if (!event) return null;
13198
+ const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
13199
+ const payload = {
13200
+ source: "canonry",
13201
+ event,
13202
+ project: { name: project.name, canonicalDomain: project.canonicalDomain },
13203
+ health: {
13204
+ status: check.status,
13205
+ code: check.code,
13206
+ summary: check.summary,
13207
+ remediation: check.remediation ?? null,
13208
+ checkedAt,
13209
+ // The counter resets on every ok, so a page always follows a site that was
13210
+ // answering. Echoing the row the first failed pass wrote would report
13211
+ // "fail -> fail" and show the operator no transition at all.
13212
+ previousStatus: event === "health.degraded" ? "ok" : "fail",
13213
+ failing: event === "health.degraded" ? [{ id: check.id, status: check.status, code: check.code, summary: check.summary }] : []
13214
+ },
13215
+ dashboardUrl: `${this.serverUrl}/projects/${project.name}`
13216
+ };
13217
+ let delivered = 0;
13218
+ for (const notif of notifs) {
13219
+ const config = notif.config;
13220
+ if (!config.url) continue;
13221
+ if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
13222
+ }
13223
+ if (event === "health.degraded" && delivered > 0) {
13224
+ this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq17(siteLivenessState.projectId, projectId)).run();
13225
+ }
13226
+ log17.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
13103
13227
  return event;
13104
13228
  }
13105
13229
  /**
@@ -13197,7 +13321,7 @@ var Notifier = class {
13197
13321
  if (!thisRun) return [];
13198
13322
  const completeness = measurementRunCompleteness(this.db, runId);
13199
13323
  if (completeness.planned && !completeness.complete) {
13200
- log16.info("transitions.skipped-incomplete", {
13324
+ log17.info("transitions.skipped-incomplete", {
13201
13325
  runId,
13202
13326
  executed: completeness.executed,
13203
13327
  expected: completeness.expected
@@ -13279,6 +13403,7 @@ var Notifier = class {
13279
13403
  }
13280
13404
  return transitions;
13281
13405
  }
13406
+ /** True only when the destination accepted the payload. Callers that record "already said" must not count an attempt. */
13282
13407
  async sendWebhook(url, payload, notificationId, projectId, webhookSecret) {
13283
13408
  const destination = resolveDestination(url);
13284
13409
  const body = destination.render ? destination.render(toAlertView(payload)) : payload;
@@ -13286,23 +13411,23 @@ var Notifier = class {
13286
13411
  const targetLabel = redactNotificationUrl(url).urlDisplay;
13287
13412
  const targetCheck = await resolveWebhookTarget(url);
13288
13413
  if (!targetCheck.ok) {
13289
- log16.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
13414
+ log17.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
13290
13415
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
13291
- return;
13416
+ return false;
13292
13417
  }
13293
- log16.info("webhook.send", { event: payload.event, url: targetLabel });
13418
+ log17.info("webhook.send", { event: payload.event, url: targetLabel });
13294
13419
  const maxRetries = 3;
13295
13420
  const delays = [1e3, 4e3, 16e3];
13296
13421
  for (let attempt = 0; attempt < maxRetries; attempt++) {
13297
13422
  try {
13298
13423
  const response = await deliverWebhook(targetCheck.target, body, signingSecret);
13299
13424
  if (response.status >= 200 && response.status < 300) {
13300
- log16.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
13425
+ log17.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
13301
13426
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
13302
- return;
13427
+ return true;
13303
13428
  }
13304
13429
  const errorDetail = response.error ?? `HTTP ${response.status}`;
13305
- log16.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
13430
+ log17.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
13306
13431
  if (attempt === maxRetries - 1) {
13307
13432
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
13308
13433
  }
@@ -13310,13 +13435,14 @@ var Notifier = class {
13310
13435
  const errorDetail = describeError(err);
13311
13436
  if (attempt === maxRetries - 1) {
13312
13437
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
13313
- log16.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
13438
+ log17.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
13314
13439
  }
13315
13440
  }
13316
13441
  if (attempt < maxRetries - 1) {
13317
13442
  await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
13318
13443
  }
13319
13444
  }
13445
+ return false;
13320
13446
  }
13321
13447
  logDelivery(projectId, notificationId, event, status, error) {
13322
13448
  this.db.insert(auditLog).values({
@@ -13334,7 +13460,7 @@ var Notifier = class {
13334
13460
 
13335
13461
  // src/run-coordinator.ts
13336
13462
  import { eq as eq18 } from "drizzle-orm";
13337
- var log17 = createLogger("RunCoordinator");
13463
+ var log18 = createLogger("RunCoordinator");
13338
13464
  var RunCoordinator = class {
13339
13465
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
13340
13466
  this.db = db;
@@ -13352,12 +13478,12 @@ var RunCoordinator = class {
13352
13478
  const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
13353
13479
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
13354
13480
  if (runRow?.trigger === RunTriggers.probe) {
13355
- log17.info("probe.skip-side-effects", { runId, projectId, kind });
13481
+ log18.info("probe.skip-side-effects", { runId, projectId, kind });
13356
13482
  return;
13357
13483
  }
13358
13484
  const completeness = measurementRunCompleteness(this.db, runId);
13359
13485
  if (completeness.planned && !completeness.complete) {
13360
- log17.info("measurement.incomplete-skip-derived", {
13486
+ log18.info("measurement.incomplete-skip-derived", {
13361
13487
  runId,
13362
13488
  projectId,
13363
13489
  executed: completeness.executed,
@@ -13366,7 +13492,7 @@ var RunCoordinator = class {
13366
13492
  try {
13367
13493
  await this.notifier.onRunCompleted(runId, projectId);
13368
13494
  } catch (err) {
13369
- log17.error("notifier.failed", { runId, error: describeError(err) });
13495
+ log18.error("notifier.failed", { runId, error: describeError(err) });
13370
13496
  }
13371
13497
  return;
13372
13498
  }
@@ -13384,12 +13510,12 @@ var RunCoordinator = class {
13384
13510
  try {
13385
13511
  await this.onInsightsGenerated(runId, projectId, result);
13386
13512
  } catch (err) {
13387
- log17.error("insight-webhook.failed", { runId, error: describeError(err) });
13513
+ log18.error("insight-webhook.failed", { runId, error: describeError(err) });
13388
13514
  }
13389
13515
  }
13390
13516
  }
13391
13517
  } catch (err) {
13392
- log17.error("intelligence.failed", { runId, error: describeError(err) });
13518
+ log18.error("intelligence.failed", { runId, error: describeError(err) });
13393
13519
  }
13394
13520
  } else if (kind === RunKinds["gbp-sync"]) {
13395
13521
  try {
@@ -13402,17 +13528,17 @@ var RunCoordinator = class {
13402
13528
  try {
13403
13529
  await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
13404
13530
  } catch (err) {
13405
- log17.error("gbp-insight-webhook.failed", { runId, error: describeError(err) });
13531
+ log18.error("gbp-insight-webhook.failed", { runId, error: describeError(err) });
13406
13532
  }
13407
13533
  }
13408
13534
  } catch (err) {
13409
- log17.error("gbp-intelligence.failed", { runId, error: describeError(err) });
13535
+ log18.error("gbp-intelligence.failed", { runId, error: describeError(err) });
13410
13536
  }
13411
13537
  }
13412
13538
  try {
13413
13539
  await this.notifier.onRunCompleted(runId, projectId);
13414
13540
  } catch (err) {
13415
- log17.error("notifier.failed", { runId, error: describeError(err) });
13541
+ log18.error("notifier.failed", { runId, error: describeError(err) });
13416
13542
  }
13417
13543
  if (this.onAeroEvent) {
13418
13544
  try {
@@ -13425,7 +13551,7 @@ var RunCoordinator = class {
13425
13551
  };
13426
13552
  await this.onAeroEvent(ctx);
13427
13553
  } catch (err) {
13428
- log17.error("aero.failed", { runId, error: describeError(err) });
13554
+ log18.error("aero.failed", { runId, error: describeError(err) });
13429
13555
  }
13430
13556
  }
13431
13557
  }
@@ -14593,7 +14719,7 @@ function resolveSessionProviderAndModel(config, opts) {
14593
14719
  import { Type as Type4 } from "@sinclair/typebox";
14594
14720
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
14595
14721
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
14596
- var log18 = createLogger("RemoteMcp");
14722
+ var log19 = createLogger("RemoteMcp");
14597
14723
  var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
14598
14724
  var MAX_TOOL_RESULT_CHARS2 = 2e4;
14599
14725
  function truncate(text) {
@@ -14646,7 +14772,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
14646
14772
  try {
14647
14773
  client = await connect(server);
14648
14774
  } catch (err) {
14649
- log18.error("external-mcp.connect-failed", {
14775
+ log19.error("external-mcp.connect-failed", {
14650
14776
  label,
14651
14777
  error: describeError(err)
14652
14778
  });
@@ -14656,7 +14782,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
14656
14782
  try {
14657
14783
  listed = await client.listTools();
14658
14784
  } catch (err) {
14659
- log18.error("external-mcp.list-failed", {
14785
+ log19.error("external-mcp.list-failed", {
14660
14786
  label,
14661
14787
  error: describeError(err)
14662
14788
  });
@@ -14667,7 +14793,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
14667
14793
  if (!isReadOnly(tool)) continue;
14668
14794
  tools.push(adaptRemoteTool(client, tool));
14669
14795
  }
14670
- log18.info("external-mcp.loaded", {
14796
+ log19.info("external-mcp.loaded", {
14671
14797
  label,
14672
14798
  discovered: listed.tools.length,
14673
14799
  adopted: tools.length
@@ -14899,7 +15025,7 @@ async function compactMessages(args) {
14899
15025
  }
14900
15026
 
14901
15027
  // src/agent/session-registry.ts
14902
- var log19 = createLogger("SessionRegistry");
15028
+ var log20 = createLogger("SessionRegistry");
14903
15029
  var MAX_HYDRATE_NOTES = 20;
14904
15030
  var MAX_HYDRATE_BYTES = 32 * 1024;
14905
15031
  function escapeMemoryFragment(value) {
@@ -14943,7 +15069,7 @@ var SessionRegistry = class {
14943
15069
  if (!this.externalToolsPromise) {
14944
15070
  this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
14945
15071
  (err) => {
14946
- log19.error("external-mcp.load-failed", {
15072
+ log20.error("external-mcp.load-failed", {
14947
15073
  error: describeError(err)
14948
15074
  });
14949
15075
  return [];
@@ -15198,13 +15324,13 @@ ${lines.join("\n")}
15198
15324
  agent.state.messages = result.messages;
15199
15325
  agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15200
15326
  this.save(projectName);
15201
- log19.info("compaction.completed", {
15327
+ log20.info("compaction.completed", {
15202
15328
  projectName,
15203
15329
  removedCount: result.removedCount,
15204
15330
  summaryBytes: Buffer.byteLength(result.summary, "utf8")
15205
15331
  });
15206
15332
  } catch (err) {
15207
- log19.error("compaction.failed", {
15333
+ log20.error("compaction.failed", {
15208
15334
  projectName,
15209
15335
  error: describeError(err)
15210
15336
  });
@@ -15305,7 +15431,7 @@ ${lines.join("\n")}
15305
15431
  await agent.prompt(msgs);
15306
15432
  this.save(projectName);
15307
15433
  } catch (err) {
15308
- log19.error("drain.failed", {
15434
+ log20.error("drain.failed", {
15309
15435
  projectName,
15310
15436
  error: describeError(err)
15311
15437
  });
@@ -16112,7 +16238,7 @@ function formatAuditFactorScore(factor) {
16112
16238
  }
16113
16239
 
16114
16240
  // src/snapshot-service.ts
16115
- var log20 = createLogger("Snapshot");
16241
+ var log21 = createLogger("Snapshot");
16116
16242
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
16117
16243
  var SNAPSHOT_QUERY_COUNT = 6;
16118
16244
  var SnapshotService = class {
@@ -16209,7 +16335,7 @@ var SnapshotService = class {
16209
16335
  return mapAuditReport(report);
16210
16336
  } catch (err) {
16211
16337
  const message = describeError(err);
16212
- log20.warn("audit.failed", { homepageUrl, error: message });
16338
+ log21.warn("audit.failed", { homepageUrl, error: message });
16213
16339
  return {
16214
16340
  url: homepageUrl,
16215
16341
  finalUrl: homepageUrl,
@@ -16238,7 +16364,7 @@ var SnapshotService = class {
16238
16364
  queries: parsedQueries
16239
16365
  };
16240
16366
  } catch (err) {
16241
- log20.warn("profile.generation-failed", {
16367
+ log21.warn("profile.generation-failed", {
16242
16368
  domain: ctx.domain,
16243
16369
  provider: ctx.analysisProvider.adapter.name,
16244
16370
  error: describeError(err)
@@ -16381,7 +16507,7 @@ var SnapshotService = class {
16381
16507
  recommendedActions: uniqueStrings2(parsed.recommendedActions ?? []).slice(0, 4)
16382
16508
  };
16383
16509
  } catch (err) {
16384
- log20.warn("response.analysis-failed", {
16510
+ log21.warn("response.analysis-failed", {
16385
16511
  provider: ctx.analysisProvider.adapter.name,
16386
16512
  error: describeError(err)
16387
16513
  });
@@ -17006,7 +17132,7 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
17006
17132
  // src/server.ts
17007
17133
  var _require3 = createRequire3(import.meta.url);
17008
17134
  var { version: PKG_VERSION2 } = _require3("../package.json");
17009
- var log21 = createLogger("Server");
17135
+ var log22 = createLogger("Server");
17010
17136
  var runtimeStartupByServer = /* @__PURE__ */ new WeakMap();
17011
17137
  function waitForServerRuntimeStartup(app) {
17012
17138
  const startup = runtimeStartupByServer.get(app);
@@ -17211,7 +17337,7 @@ function applyLegacyCredentials(rows, config) {
17211
17337
  }
17212
17338
  if (migratedGoogle > 0) {
17213
17339
  saveConfigPatch({ google: config.google });
17214
- log21.info("credentials.migrated", { type: "google", count: migratedGoogle });
17340
+ log22.info("credentials.migrated", { type: "google", count: migratedGoogle });
17215
17341
  }
17216
17342
  let migratedGa4 = 0;
17217
17343
  for (const row of rows.ga4) {
@@ -17229,7 +17355,7 @@ function applyLegacyCredentials(rows, config) {
17229
17355
  }
17230
17356
  if (migratedGa4 > 0) {
17231
17357
  saveConfigPatch({ ga4: config.ga4 });
17232
- log21.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
17358
+ log22.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
17233
17359
  }
17234
17360
  }
17235
17361
  function isLoopbackBindHost(host) {
@@ -17404,11 +17530,11 @@ async function createServer(opts) {
17404
17530
  applyLegacyCredentials(legacyRows, opts.config);
17405
17531
  dropLegacyCredentialColumns(opts.db);
17406
17532
  } catch (err) {
17407
- log21.warn("credentials.migration.failed", {
17533
+ log22.warn("credentials.migration.failed", {
17408
17534
  error: describeError(err)
17409
17535
  });
17410
17536
  }
17411
- log21.info("providers.configured", {
17537
+ log22.info("providers.configured", {
17412
17538
  providers: Object.keys(providers).filter((k) => {
17413
17539
  const p = providers[k];
17414
17540
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
@@ -17890,6 +18016,7 @@ async function createServer(opts) {
17890
18016
  AD_GROUP_IN_PROGRESS_INSIGHT_FIELDS
17891
18017
  ))
17892
18018
  };
18019
+ let stopSiteLiveness = null;
17893
18020
  const scheduler = new Scheduler(opts.db, {
17894
18021
  onRunCreated: (runId, projectId, providers2, location) => {
17895
18022
  jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
@@ -19261,6 +19388,18 @@ async function createServer(opts) {
19261
19388
  if (runtimeStartupSettled) return;
19262
19389
  try {
19263
19390
  scheduler.start();
19391
+ stopSiteLiveness = startSiteLivenessLoop({
19392
+ db: opts.db,
19393
+ // In-process, so a pass writes no HTTP request logs, and the check is
19394
+ // named explicitly because it is opt-in and never runs in a default pass.
19395
+ probe: async (project) => {
19396
+ const report = await runChecks({ db: opts.db, project }, SITE_REACHABILITY_CHECKS, {
19397
+ checkIds: [SITE_REACHABILITY_CHECK_ID]
19398
+ });
19399
+ return report.checks[0] ?? null;
19400
+ },
19401
+ notify: (projectId, result) => notifier.onSiteLivenessChecked(projectId, result)
19402
+ });
19264
19403
  for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq25(researchRuns.status, ResearchRunStatuses.queued)).all()) {
19265
19404
  dispatchResearchRun(run.id, run.projectId);
19266
19405
  }
@@ -19274,6 +19413,8 @@ async function createServer(opts) {
19274
19413
  const stopLogCapture = addLogListener((entry) => operationalLogs.append(entry));
19275
19414
  app.addHook("onClose", async () => {
19276
19415
  stopLogCapture();
19416
+ stopSiteLiveness?.();
19417
+ stopSiteLiveness = null;
19277
19418
  scheduler.stop();
19278
19419
  });
19279
19420
  return app;
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  trackCliCommandFinished,
34
34
  trackEvent,
35
35
  waitForServerRuntimeStartup
36
- } from "./chunk-AGUCTX7L.js";
36
+ } from "./chunk-TF6HFT3Q.js";
37
37
  import {
38
38
  autoSyncSkills,
39
39
  formatAutoSyncNotice
@@ -69,7 +69,7 @@ import {
69
69
  trustedProxiesProblem,
70
70
  trustedProxyProblem
71
71
  } from "./chunk-RB2F4D3Z.js";
72
- import "./chunk-5VONCLVQ.js";
72
+ import "./chunk-3COTRCK3.js";
73
73
  import "./chunk-AOJU5LKX.js";
74
74
  import {
75
75
  CLOUDFLARE_WORKER_BINDINGS,
@@ -84,7 +84,7 @@ import {
84
84
  projects,
85
85
  queries,
86
86
  renderReportHtml
87
- } from "./chunk-YQNKULAD.js";
87
+ } from "./chunk-IACO5R7A.js";
88
88
  import {
89
89
  AdsDeliverySnapshotStatuses,
90
90
  AdsHistoricalCampaignRollupStatuses,
@@ -428,7 +428,7 @@ function parseDemoTrustedProxies(values) {
428
428
  async function demoCommand(options) {
429
429
  const { host, port } = parseDemoListenOptions(options);
430
430
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
431
- const { createDemoServer } = await import("./demo-server-BIPT53EY.js");
431
+ const { createDemoServer } = await import("./demo-server-TXW3UX3P.js");
432
432
  const app = await createDemoServer({ trustProxy });
433
433
  let stopping = false;
434
434
  const stop = () => {
@@ -11021,8 +11021,8 @@ var EVENT_DESCRIPTIONS = {
11021
11021
  "run.failed": "An AEO sweep failed",
11022
11022
  "insight.critical": "A critical-severity insight was generated",
11023
11023
  "insight.high": "A high-severity insight was generated",
11024
- "health.degraded": "A scheduled health check found the measurement itself degraded (sent on change, not every pass)",
11025
- "health.recovered": "A previously degraded health check is passing again"
11024
+ "health.degraded": "A scheduled health check found the measurement degraded, daily data stopped arriving, or the website stopped answering (sent on change, not every pass)",
11025
+ "health.recovered": "A degraded health check is passing again, or a website that was paged as down answers again"
11026
11026
  };
11027
11027
  function listEvents(format, target = "local") {
11028
11028
  if (target === "server") return getClient16().listNotificationEvents().then((events) => printEvents(events, format));