@ainyc/canonry 5.2.1 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/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-AGUCTX7L.js → chunk-HOOU4NS2.js} +194 -56
  21. package/dist/{chunk-5VONCLVQ.js → chunk-MNUFMEAX.js} +1 -1
  22. package/dist/{chunk-YQNKULAD.js → chunk-QAGZWYPI.js} +815 -366
  23. package/dist/cli.js +6 -6
  24. package/dist/{demo-server-BIPT53EY.js → demo-server-Y7YWLPU7.js} +2 -2
  25. package/dist/index.js +3 -3
  26. package/dist/{intelligence-service-SJO5E7WN.js → intelligence-service-PR6T6JKY.js} +1 -1
  27. package/package.json +8 -8
  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-MNUFMEAX.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-QAGZWYPI.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 ? "caba5d386b2538f0fb2e4b23c1275a1c8c652fbd" : 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-PR6T6JKY.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;
@@ -13070,7 +13117,7 @@ var Notifier = class {
13070
13117
  this.db.update(doctorHealthState).set(observation).where(eq17(doctorHealthState.projectId, projectId)).run();
13071
13118
  }
13072
13119
  if (!event) {
13073
- log16.info("health.unchanged", { projectId, status, code });
13120
+ log17.info("health.unchanged", { projectId, status, code });
13074
13121
  return null;
13075
13122
  }
13076
13123
  const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
@@ -13093,13 +13140,87 @@ var Notifier = class {
13093
13140
  for (const notif of notifs) {
13094
13141
  const config = notif.config;
13095
13142
  if (!config.url) continue;
13096
- await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
13097
- delivered += 1;
13143
+ if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
13098
13144
  }
13099
13145
  if (delivered > 0) {
13100
13146
  this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq17(doctorHealthState.projectId, projectId)).run();
13101
13147
  }
13102
- log16.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
13148
+ log17.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
13149
+ return event;
13150
+ }
13151
+ /**
13152
+ * Record a website liveness probe and notify on a confirmed change.
13153
+ *
13154
+ * Separate from onHealthChecked: that state grades the whole 6h doctor pass,
13155
+ * and this pass sees one check. Sharing its row would let "site is up" clear
13156
+ * an unrelated GA outage and send a false recovery.
13157
+ *
13158
+ * Two failed passes in a row before paging. The probe already retries once
13159
+ * inside a pass, so an alert means four failed requests spread over at least
13160
+ * one schedule interval, not a blip or a host restart. A recovery is sent only
13161
+ * for an outage that was paged, so a blip that never alerted stays silent in
13162
+ * both directions.
13163
+ */
13164
+ async onSiteLivenessChecked(projectId, result) {
13165
+ const { check, checkedAt } = result;
13166
+ if (check.status !== "ok" && check.status !== "fail") return null;
13167
+ const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
13168
+ if (!project) {
13169
+ log17.error("project.not-found", { projectId, msg: "skipping site liveness notification" });
13170
+ return null;
13171
+ }
13172
+ const previous = this.db.select().from(siteLivenessState).where(eq17(siteLivenessState.projectId, projectId)).get();
13173
+ const sincePrevious = previous ? Date.parse(checkedAt) - Date.parse(previous.checkedAt) : Number.NaN;
13174
+ const countsAsRepeat = Number.isFinite(sincePrevious) && sincePrevious >= SITE_LIVENESS_MIN_PASS_GAP_MS && sincePrevious <= SITE_LIVENESS_MAX_PASS_GAP_MS;
13175
+ const failures = check.status === "fail" ? countsAsRepeat ? (previous?.consecutiveFailures ?? 0) + 1 : 1 : 0;
13176
+ const outagePaged = previous?.status === "fail" && Boolean(previous.notifiedAt);
13177
+ let event = null;
13178
+ if (check.status === "fail" && failures >= SITE_LIVENESS_FAILURES_TO_PAGE && !outagePaged) event = "health.degraded";
13179
+ if (check.status === "ok" && outagePaged) event = "health.recovered";
13180
+ const observation = {
13181
+ projectId,
13182
+ status: check.status,
13183
+ code: check.code,
13184
+ summary: check.summary,
13185
+ consecutiveFailures: failures,
13186
+ checkedAt,
13187
+ notifiedAt: check.status === "ok" ? null : previous?.notifiedAt ?? null
13188
+ };
13189
+ if (previous === void 0) {
13190
+ this.db.insert(siteLivenessState).values(observation).run();
13191
+ } else {
13192
+ this.db.update(siteLivenessState).set(observation).where(eq17(siteLivenessState.projectId, projectId)).run();
13193
+ }
13194
+ if (!event) return null;
13195
+ const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
13196
+ const payload = {
13197
+ source: "canonry",
13198
+ event,
13199
+ project: { name: project.name, canonicalDomain: project.canonicalDomain },
13200
+ health: {
13201
+ status: check.status,
13202
+ code: check.code,
13203
+ summary: check.summary,
13204
+ remediation: check.remediation ?? null,
13205
+ checkedAt,
13206
+ // The counter resets on every ok, so a page always follows a site that was
13207
+ // answering. Echoing the row the first failed pass wrote would report
13208
+ // "fail -> fail" and show the operator no transition at all.
13209
+ previousStatus: event === "health.degraded" ? "ok" : "fail",
13210
+ failing: event === "health.degraded" ? [{ id: check.id, status: check.status, code: check.code, summary: check.summary }] : []
13211
+ },
13212
+ dashboardUrl: `${this.serverUrl}/projects/${project.name}`
13213
+ };
13214
+ let delivered = 0;
13215
+ for (const notif of notifs) {
13216
+ const config = notif.config;
13217
+ if (!config.url) continue;
13218
+ if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
13219
+ }
13220
+ if (event === "health.degraded" && delivered > 0) {
13221
+ this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq17(siteLivenessState.projectId, projectId)).run();
13222
+ }
13223
+ log17.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
13103
13224
  return event;
13104
13225
  }
13105
13226
  /**
@@ -13197,7 +13318,7 @@ var Notifier = class {
13197
13318
  if (!thisRun) return [];
13198
13319
  const completeness = measurementRunCompleteness(this.db, runId);
13199
13320
  if (completeness.planned && !completeness.complete) {
13200
- log16.info("transitions.skipped-incomplete", {
13321
+ log17.info("transitions.skipped-incomplete", {
13201
13322
  runId,
13202
13323
  executed: completeness.executed,
13203
13324
  expected: completeness.expected
@@ -13279,6 +13400,7 @@ var Notifier = class {
13279
13400
  }
13280
13401
  return transitions;
13281
13402
  }
13403
+ /** True only when the destination accepted the payload. Callers that record "already said" must not count an attempt. */
13282
13404
  async sendWebhook(url, payload, notificationId, projectId, webhookSecret) {
13283
13405
  const destination = resolveDestination(url);
13284
13406
  const body = destination.render ? destination.render(toAlertView(payload)) : payload;
@@ -13286,23 +13408,23 @@ var Notifier = class {
13286
13408
  const targetLabel = redactNotificationUrl(url).urlDisplay;
13287
13409
  const targetCheck = await resolveWebhookTarget(url);
13288
13410
  if (!targetCheck.ok) {
13289
- log16.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
13411
+ log17.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
13290
13412
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
13291
- return;
13413
+ return false;
13292
13414
  }
13293
- log16.info("webhook.send", { event: payload.event, url: targetLabel });
13415
+ log17.info("webhook.send", { event: payload.event, url: targetLabel });
13294
13416
  const maxRetries = 3;
13295
13417
  const delays = [1e3, 4e3, 16e3];
13296
13418
  for (let attempt = 0; attempt < maxRetries; attempt++) {
13297
13419
  try {
13298
13420
  const response = await deliverWebhook(targetCheck.target, body, signingSecret);
13299
13421
  if (response.status >= 200 && response.status < 300) {
13300
- log16.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
13422
+ log17.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
13301
13423
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
13302
- return;
13424
+ return true;
13303
13425
  }
13304
13426
  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 });
13427
+ log17.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
13306
13428
  if (attempt === maxRetries - 1) {
13307
13429
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
13308
13430
  }
@@ -13310,13 +13432,14 @@ var Notifier = class {
13310
13432
  const errorDetail = describeError(err);
13311
13433
  if (attempt === maxRetries - 1) {
13312
13434
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
13313
- log16.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
13435
+ log17.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
13314
13436
  }
13315
13437
  }
13316
13438
  if (attempt < maxRetries - 1) {
13317
13439
  await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
13318
13440
  }
13319
13441
  }
13442
+ return false;
13320
13443
  }
13321
13444
  logDelivery(projectId, notificationId, event, status, error) {
13322
13445
  this.db.insert(auditLog).values({
@@ -13334,7 +13457,7 @@ var Notifier = class {
13334
13457
 
13335
13458
  // src/run-coordinator.ts
13336
13459
  import { eq as eq18 } from "drizzle-orm";
13337
- var log17 = createLogger("RunCoordinator");
13460
+ var log18 = createLogger("RunCoordinator");
13338
13461
  var RunCoordinator = class {
13339
13462
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
13340
13463
  this.db = db;
@@ -13352,12 +13475,12 @@ var RunCoordinator = class {
13352
13475
  const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
13353
13476
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
13354
13477
  if (runRow?.trigger === RunTriggers.probe) {
13355
- log17.info("probe.skip-side-effects", { runId, projectId, kind });
13478
+ log18.info("probe.skip-side-effects", { runId, projectId, kind });
13356
13479
  return;
13357
13480
  }
13358
13481
  const completeness = measurementRunCompleteness(this.db, runId);
13359
13482
  if (completeness.planned && !completeness.complete) {
13360
- log17.info("measurement.incomplete-skip-derived", {
13483
+ log18.info("measurement.incomplete-skip-derived", {
13361
13484
  runId,
13362
13485
  projectId,
13363
13486
  executed: completeness.executed,
@@ -13366,7 +13489,7 @@ var RunCoordinator = class {
13366
13489
  try {
13367
13490
  await this.notifier.onRunCompleted(runId, projectId);
13368
13491
  } catch (err) {
13369
- log17.error("notifier.failed", { runId, error: describeError(err) });
13492
+ log18.error("notifier.failed", { runId, error: describeError(err) });
13370
13493
  }
13371
13494
  return;
13372
13495
  }
@@ -13384,12 +13507,12 @@ var RunCoordinator = class {
13384
13507
  try {
13385
13508
  await this.onInsightsGenerated(runId, projectId, result);
13386
13509
  } catch (err) {
13387
- log17.error("insight-webhook.failed", { runId, error: describeError(err) });
13510
+ log18.error("insight-webhook.failed", { runId, error: describeError(err) });
13388
13511
  }
13389
13512
  }
13390
13513
  }
13391
13514
  } catch (err) {
13392
- log17.error("intelligence.failed", { runId, error: describeError(err) });
13515
+ log18.error("intelligence.failed", { runId, error: describeError(err) });
13393
13516
  }
13394
13517
  } else if (kind === RunKinds["gbp-sync"]) {
13395
13518
  try {
@@ -13402,17 +13525,17 @@ var RunCoordinator = class {
13402
13525
  try {
13403
13526
  await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
13404
13527
  } catch (err) {
13405
- log17.error("gbp-insight-webhook.failed", { runId, error: describeError(err) });
13528
+ log18.error("gbp-insight-webhook.failed", { runId, error: describeError(err) });
13406
13529
  }
13407
13530
  }
13408
13531
  } catch (err) {
13409
- log17.error("gbp-intelligence.failed", { runId, error: describeError(err) });
13532
+ log18.error("gbp-intelligence.failed", { runId, error: describeError(err) });
13410
13533
  }
13411
13534
  }
13412
13535
  try {
13413
13536
  await this.notifier.onRunCompleted(runId, projectId);
13414
13537
  } catch (err) {
13415
- log17.error("notifier.failed", { runId, error: describeError(err) });
13538
+ log18.error("notifier.failed", { runId, error: describeError(err) });
13416
13539
  }
13417
13540
  if (this.onAeroEvent) {
13418
13541
  try {
@@ -13425,7 +13548,7 @@ var RunCoordinator = class {
13425
13548
  };
13426
13549
  await this.onAeroEvent(ctx);
13427
13550
  } catch (err) {
13428
- log17.error("aero.failed", { runId, error: describeError(err) });
13551
+ log18.error("aero.failed", { runId, error: describeError(err) });
13429
13552
  }
13430
13553
  }
13431
13554
  }
@@ -14593,7 +14716,7 @@ function resolveSessionProviderAndModel(config, opts) {
14593
14716
  import { Type as Type4 } from "@sinclair/typebox";
14594
14717
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
14595
14718
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
14596
- var log18 = createLogger("RemoteMcp");
14719
+ var log19 = createLogger("RemoteMcp");
14597
14720
  var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
14598
14721
  var MAX_TOOL_RESULT_CHARS2 = 2e4;
14599
14722
  function truncate(text) {
@@ -14646,7 +14769,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
14646
14769
  try {
14647
14770
  client = await connect(server);
14648
14771
  } catch (err) {
14649
- log18.error("external-mcp.connect-failed", {
14772
+ log19.error("external-mcp.connect-failed", {
14650
14773
  label,
14651
14774
  error: describeError(err)
14652
14775
  });
@@ -14656,7 +14779,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
14656
14779
  try {
14657
14780
  listed = await client.listTools();
14658
14781
  } catch (err) {
14659
- log18.error("external-mcp.list-failed", {
14782
+ log19.error("external-mcp.list-failed", {
14660
14783
  label,
14661
14784
  error: describeError(err)
14662
14785
  });
@@ -14667,7 +14790,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
14667
14790
  if (!isReadOnly(tool)) continue;
14668
14791
  tools.push(adaptRemoteTool(client, tool));
14669
14792
  }
14670
- log18.info("external-mcp.loaded", {
14793
+ log19.info("external-mcp.loaded", {
14671
14794
  label,
14672
14795
  discovered: listed.tools.length,
14673
14796
  adopted: tools.length
@@ -14899,7 +15022,7 @@ async function compactMessages(args) {
14899
15022
  }
14900
15023
 
14901
15024
  // src/agent/session-registry.ts
14902
- var log19 = createLogger("SessionRegistry");
15025
+ var log20 = createLogger("SessionRegistry");
14903
15026
  var MAX_HYDRATE_NOTES = 20;
14904
15027
  var MAX_HYDRATE_BYTES = 32 * 1024;
14905
15028
  function escapeMemoryFragment(value) {
@@ -14943,7 +15066,7 @@ var SessionRegistry = class {
14943
15066
  if (!this.externalToolsPromise) {
14944
15067
  this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
14945
15068
  (err) => {
14946
- log19.error("external-mcp.load-failed", {
15069
+ log20.error("external-mcp.load-failed", {
14947
15070
  error: describeError(err)
14948
15071
  });
14949
15072
  return [];
@@ -15198,13 +15321,13 @@ ${lines.join("\n")}
15198
15321
  agent.state.messages = result.messages;
15199
15322
  agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15200
15323
  this.save(projectName);
15201
- log19.info("compaction.completed", {
15324
+ log20.info("compaction.completed", {
15202
15325
  projectName,
15203
15326
  removedCount: result.removedCount,
15204
15327
  summaryBytes: Buffer.byteLength(result.summary, "utf8")
15205
15328
  });
15206
15329
  } catch (err) {
15207
- log19.error("compaction.failed", {
15330
+ log20.error("compaction.failed", {
15208
15331
  projectName,
15209
15332
  error: describeError(err)
15210
15333
  });
@@ -15305,7 +15428,7 @@ ${lines.join("\n")}
15305
15428
  await agent.prompt(msgs);
15306
15429
  this.save(projectName);
15307
15430
  } catch (err) {
15308
- log19.error("drain.failed", {
15431
+ log20.error("drain.failed", {
15309
15432
  projectName,
15310
15433
  error: describeError(err)
15311
15434
  });
@@ -16112,7 +16235,7 @@ function formatAuditFactorScore(factor) {
16112
16235
  }
16113
16236
 
16114
16237
  // src/snapshot-service.ts
16115
- var log20 = createLogger("Snapshot");
16238
+ var log21 = createLogger("Snapshot");
16116
16239
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
16117
16240
  var SNAPSHOT_QUERY_COUNT = 6;
16118
16241
  var SnapshotService = class {
@@ -16209,7 +16332,7 @@ var SnapshotService = class {
16209
16332
  return mapAuditReport(report);
16210
16333
  } catch (err) {
16211
16334
  const message = describeError(err);
16212
- log20.warn("audit.failed", { homepageUrl, error: message });
16335
+ log21.warn("audit.failed", { homepageUrl, error: message });
16213
16336
  return {
16214
16337
  url: homepageUrl,
16215
16338
  finalUrl: homepageUrl,
@@ -16238,7 +16361,7 @@ var SnapshotService = class {
16238
16361
  queries: parsedQueries
16239
16362
  };
16240
16363
  } catch (err) {
16241
- log20.warn("profile.generation-failed", {
16364
+ log21.warn("profile.generation-failed", {
16242
16365
  domain: ctx.domain,
16243
16366
  provider: ctx.analysisProvider.adapter.name,
16244
16367
  error: describeError(err)
@@ -16381,7 +16504,7 @@ var SnapshotService = class {
16381
16504
  recommendedActions: uniqueStrings2(parsed.recommendedActions ?? []).slice(0, 4)
16382
16505
  };
16383
16506
  } catch (err) {
16384
- log20.warn("response.analysis-failed", {
16507
+ log21.warn("response.analysis-failed", {
16385
16508
  provider: ctx.analysisProvider.adapter.name,
16386
16509
  error: describeError(err)
16387
16510
  });
@@ -17006,7 +17129,7 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
17006
17129
  // src/server.ts
17007
17130
  var _require3 = createRequire3(import.meta.url);
17008
17131
  var { version: PKG_VERSION2 } = _require3("../package.json");
17009
- var log21 = createLogger("Server");
17132
+ var log22 = createLogger("Server");
17010
17133
  var runtimeStartupByServer = /* @__PURE__ */ new WeakMap();
17011
17134
  function waitForServerRuntimeStartup(app) {
17012
17135
  const startup = runtimeStartupByServer.get(app);
@@ -17211,7 +17334,7 @@ function applyLegacyCredentials(rows, config) {
17211
17334
  }
17212
17335
  if (migratedGoogle > 0) {
17213
17336
  saveConfigPatch({ google: config.google });
17214
- log21.info("credentials.migrated", { type: "google", count: migratedGoogle });
17337
+ log22.info("credentials.migrated", { type: "google", count: migratedGoogle });
17215
17338
  }
17216
17339
  let migratedGa4 = 0;
17217
17340
  for (const row of rows.ga4) {
@@ -17229,7 +17352,7 @@ function applyLegacyCredentials(rows, config) {
17229
17352
  }
17230
17353
  if (migratedGa4 > 0) {
17231
17354
  saveConfigPatch({ ga4: config.ga4 });
17232
- log21.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
17355
+ log22.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
17233
17356
  }
17234
17357
  }
17235
17358
  function isLoopbackBindHost(host) {
@@ -17404,11 +17527,11 @@ async function createServer(opts) {
17404
17527
  applyLegacyCredentials(legacyRows, opts.config);
17405
17528
  dropLegacyCredentialColumns(opts.db);
17406
17529
  } catch (err) {
17407
- log21.warn("credentials.migration.failed", {
17530
+ log22.warn("credentials.migration.failed", {
17408
17531
  error: describeError(err)
17409
17532
  });
17410
17533
  }
17411
- log21.info("providers.configured", {
17534
+ log22.info("providers.configured", {
17412
17535
  providers: Object.keys(providers).filter((k) => {
17413
17536
  const p = providers[k];
17414
17537
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
@@ -17890,6 +18013,7 @@ async function createServer(opts) {
17890
18013
  AD_GROUP_IN_PROGRESS_INSIGHT_FIELDS
17891
18014
  ))
17892
18015
  };
18016
+ let stopSiteLiveness = null;
17893
18017
  const scheduler = new Scheduler(opts.db, {
17894
18018
  onRunCreated: (runId, projectId, providers2, location) => {
17895
18019
  jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
@@ -19261,6 +19385,18 @@ async function createServer(opts) {
19261
19385
  if (runtimeStartupSettled) return;
19262
19386
  try {
19263
19387
  scheduler.start();
19388
+ stopSiteLiveness = startSiteLivenessLoop({
19389
+ db: opts.db,
19390
+ // In-process, so a pass writes no HTTP request logs, and the check is
19391
+ // named explicitly because it is opt-in and never runs in a default pass.
19392
+ probe: async (project) => {
19393
+ const report = await runChecks({ db: opts.db, project }, SITE_REACHABILITY_CHECKS, {
19394
+ checkIds: [SITE_REACHABILITY_CHECK_ID]
19395
+ });
19396
+ return report.checks[0] ?? null;
19397
+ },
19398
+ notify: (projectId, result) => notifier.onSiteLivenessChecked(projectId, result)
19399
+ });
19264
19400
  for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq25(researchRuns.status, ResearchRunStatuses.queued)).all()) {
19265
19401
  dispatchResearchRun(run.id, run.projectId);
19266
19402
  }
@@ -19274,6 +19410,8 @@ async function createServer(opts) {
19274
19410
  const stopLogCapture = addLogListener((entry) => operationalLogs.append(entry));
19275
19411
  app.addHook("onClose", async () => {
19276
19412
  stopLogCapture();
19413
+ stopSiteLiveness?.();
19414
+ stopSiteLiveness = null;
19277
19415
  scheduler.stop();
19278
19416
  });
19279
19417
  return app;
@@ -5,7 +5,7 @@ import {
5
5
  siteCrawlGraphNodes,
6
6
  siteCrawlPages,
7
7
  siteCrawlSnapshots
8
- } from "./chunk-YQNKULAD.js";
8
+ } from "./chunk-QAGZWYPI.js";
9
9
  import {
10
10
  SITE_CRAWL_GRAPH_MAX_EDGES,
11
11
  SITE_CRAWL_GRAPH_MAX_NODES,