@ainyc/canonry 4.138.0 → 4.139.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 (41) hide show
  1. package/README.md +1 -1
  2. package/assets/agent-workspace/skills/aero/references/reporting.md +22 -1
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -13
  4. package/assets/assets/{AuditHistoryPanel-CIdoSKjP.js → AuditHistoryPanel-FiMbN4_a.js} +1 -1
  5. package/assets/assets/BacklinksPage-CLRIsKHN.js +1 -0
  6. package/assets/assets/ChartPrimitives-BuTqG5NR.js +1 -0
  7. package/assets/assets/HistoryPage-BWRbsFAT.js +1 -0
  8. package/assets/assets/ProjectPage-DQ2kIj-L.js +8 -0
  9. package/assets/assets/{RunRow-D6W6D1BX.js → RunRow-BZobaeDq.js} +1 -1
  10. package/assets/assets/RunsPage-DhrY4Ouc.js +1 -0
  11. package/assets/assets/SettingsPage-C6VcJEgB.js +1 -0
  12. package/assets/assets/TrafficPage-KqYl5HUH.js +1 -0
  13. package/assets/assets/TrafficSourceDetailPage-mMJi-QZr.js +1 -0
  14. package/assets/assets/{arrow-left-ClC65OiR.js → arrow-left-CpLow2Kp.js} +1 -1
  15. package/assets/assets/extract-error-message-dHq11Y4U.js +1 -0
  16. package/assets/assets/index-CntuM6Th.css +1 -0
  17. package/assets/assets/index-iRW7X23W.js +213 -0
  18. package/assets/assets/trash-2-CnEPNBqu.js +1 -0
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-GO24URVP.js → chunk-DJHSOPMU.js} +1609 -1149
  21. package/dist/{chunk-R3GAWKHM.js → chunk-IYTHIAAE.js} +5 -25
  22. package/dist/{chunk-RJ7OW6W6.js → chunk-OA2EGCUS.js} +2973 -2914
  23. package/dist/{chunk-3KGJ7BXW.js → chunk-YLHZQ7HQ.js} +567 -435
  24. package/dist/cli.js +10 -38
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-5RJHJLYA.js → intelligence-service-BGH2MGHB.js} +2 -2
  28. package/dist/mcp.js +2 -2
  29. package/package.json +8 -8
  30. package/assets/assets/BacklinksPage-BYnzQLFq.js +0 -1
  31. package/assets/assets/ChartPrimitives-bx4p5MDx.js +0 -1
  32. package/assets/assets/HistoryPage-CjLb8TKY.js +0 -1
  33. package/assets/assets/ProjectPage-C_bSGiLs.js +0 -8
  34. package/assets/assets/RunsPage-CQT4lXFp.js +0 -1
  35. package/assets/assets/SettingsPage-DVQKzz6s.js +0 -1
  36. package/assets/assets/TrafficPage-BdocLgWw.js +0 -1
  37. package/assets/assets/TrafficSourceDetailPage-CTZF_R9C.js +0 -1
  38. package/assets/assets/extract-error-message-CsgTjzyj.js +0 -1
  39. package/assets/assets/index-BZgknVrZ.js +0 -213
  40. package/assets/assets/index-DY7GziwY.css +0 -1
  41. package/assets/assets/trash-2-nr8RRH47.js +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-R3GAWKHM.js";
13
+ } from "./chunk-IYTHIAAE.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -50,8 +50,10 @@ import {
50
50
  deliverWebhook,
51
51
  determineCitationState,
52
52
  discoverySessions,
53
+ doctorHealthState,
53
54
  downloadFile,
54
55
  dropLegacyCredentialColumns,
56
+ ensureCurrentQueryBasketRevision,
55
57
  executeDiscovery,
56
58
  extractLegacyCredentials,
57
59
  extractRecommendedCompetitors,
@@ -70,11 +72,9 @@ import {
70
72
  gbpPlaceDetails,
71
73
  getAttributes,
72
74
  getCrawlIssues,
73
- getLinkCounts,
74
75
  getLodging,
75
76
  getPlaceDetails,
76
77
  getUrlInfo,
77
- getUrlLinks,
78
78
  groupRunsByCreatedAt,
79
79
  gscCoverageSnapshots,
80
80
  gscDailyTotals,
@@ -89,6 +89,7 @@ import {
89
89
  installDuckdb,
90
90
  isDuckdbInstalled,
91
91
  isValidReleaseId,
92
+ latestQueryBasketRevision,
92
93
  listCachedReleases,
93
94
  listLocations,
94
95
  listMonthlyKeywords,
@@ -113,13 +114,16 @@ import {
113
114
  refreshAccessToken,
114
115
  researchRunQueries,
115
116
  researchRuns,
117
+ resolveDestination,
118
+ resolveVercelSyncDeadlineMs,
116
119
  resolveWebhookTarget,
117
120
  runs,
118
121
  schedules,
119
122
  siteAuditPages,
120
123
  siteAuditSnapshots,
124
+ toAlertView,
121
125
  usageCounters
122
- } from "./chunk-GO24URVP.js";
126
+ } from "./chunk-DJHSOPMU.js";
123
127
  import {
124
128
  AGENT_MEMORY_VALUE_MAX_BYTES,
125
129
  AGENT_PROVIDER_IDS,
@@ -206,7 +210,7 @@ import {
206
210
  validationError,
207
211
  winnabilityClassLabel,
208
212
  withRetry
209
- } from "./chunk-RJ7OW6W6.js";
213
+ } from "./chunk-OA2EGCUS.js";
210
214
 
211
215
  // src/telemetry.ts
212
216
  import crypto from "crypto";
@@ -537,11 +541,11 @@ function checkLatestVersionForServer(opts) {
537
541
 
538
542
  // src/server.ts
539
543
  import { createRequire as createRequire3 } from "module";
540
- import crypto23 from "crypto";
544
+ import crypto22 from "crypto";
541
545
  import fs8 from "fs";
542
546
  import path9 from "path";
543
547
  import { fileURLToPath as fileURLToPath3 } from "url";
544
- import { and as and16, eq as eq22 } from "drizzle-orm";
548
+ import { and as and15, eq as eq21 } from "drizzle-orm";
545
549
  import Fastify from "fastify";
546
550
  import os5 from "os";
547
551
 
@@ -958,7 +962,14 @@ var geminiAdapter = {
958
962
  model: raw.model,
959
963
  servedModel: raw.servedModel,
960
964
  groundingSources: raw.groundingSources,
961
- searchQueries: raw.searchQueries
965
+ searchQueries: raw.searchQueries,
966
+ // Retrieval detection is not implemented for this provider. Its candidate
967
+ // marker is present on 100% of stored rows, so it has never been shown to
968
+ // discriminate a non-retrieving answer and wiring it up would hardcode
969
+ // `used`. `unknown` states what we actually know. The contract is a
970
+ // declaration about how we build the request, so it is always knowable.
971
+ retrievalStatus: "unknown",
972
+ retrievalContract: "native-auto-v1"
962
973
  };
963
974
  },
964
975
  normalizeResult(raw) {
@@ -975,7 +986,8 @@ var geminiAdapter = {
975
986
  answerText: normalized.answerText,
976
987
  citedDomains: normalized.citedDomains,
977
988
  groundingSources: normalized.groundingSources,
978
- searchQueries: normalized.searchQueries
989
+ searchQueries: normalized.searchQueries,
990
+ retrievalStatus: "unknown"
979
991
  };
980
992
  },
981
993
  async generateText(prompt, config) {
@@ -1300,7 +1312,14 @@ var openaiAdapter = {
1300
1312
  model: raw.model,
1301
1313
  servedModel: raw.servedModel,
1302
1314
  groundingSources: raw.groundingSources,
1303
- searchQueries: raw.searchQueries
1315
+ searchQueries: raw.searchQueries,
1316
+ // Retrieval detection is not implemented for this provider. Its candidate
1317
+ // marker is present on 100% of stored rows, so it has never been shown to
1318
+ // discriminate a non-retrieving answer and wiring it up would hardcode
1319
+ // `used`. `unknown` states what we actually know. The contract is a
1320
+ // declaration about how we build the request, so it is always knowable.
1321
+ retrievalStatus: "unknown",
1322
+ retrievalContract: "native-auto-v1"
1304
1323
  };
1305
1324
  },
1306
1325
  normalizeResult(raw) {
@@ -1317,7 +1336,8 @@ var openaiAdapter = {
1317
1336
  answerText: normalized.answerText,
1318
1337
  citedDomains: normalized.citedDomains,
1319
1338
  groundingSources: normalized.groundingSources,
1320
- searchQueries: normalized.searchQueries
1339
+ searchQueries: normalized.searchQueries,
1340
+ retrievalStatus: "unknown"
1321
1341
  };
1322
1342
  },
1323
1343
  async generateText(prompt, config) {
@@ -1350,6 +1370,7 @@ async function withRetry4(fn, options = {}) {
1350
1370
  // ../provider-claude/src/normalize.ts
1351
1371
  var DEFAULT_MODEL3 = "claude-sonnet-4-6";
1352
1372
  var VALIDATION_PATTERN = /^claude-/;
1373
+ var CLAUDE_RETRIEVAL_CONTRACT = "search-required-v1";
1353
1374
  function resolveModel2(config) {
1354
1375
  const m = config.model;
1355
1376
  if (!m) return DEFAULT_MODEL3;
@@ -1424,6 +1445,9 @@ async function executeTrackedQuery3(input) {
1424
1445
  model,
1425
1446
  max_tokens: 4096,
1426
1447
  tools: [webSearchTool],
1448
+ // search-required-v1: retrieval is guaranteed by the API control, so the
1449
+ // query text and answer substance stay untouched. See the contract note.
1450
+ tool_choice: { type: "tool", name: "web_search" },
1427
1451
  messages: [{ role: "user", content: input.query }]
1428
1452
  })
1429
1453
  );
@@ -1438,7 +1462,9 @@ async function executeTrackedQuery3(input) {
1438
1462
  model,
1439
1463
  servedModel: extractServedModel3(rawResponse),
1440
1464
  groundingSources: parsed.groundingSources,
1441
- searchQueries: parsed.searchQueries
1465
+ searchQueries: parsed.searchQueries,
1466
+ retrievalStatus: parsed.retrievalStatus,
1467
+ retrievalContract: CLAUDE_RETRIEVAL_CONTRACT
1442
1468
  };
1443
1469
  } catch (err) {
1444
1470
  const msg = err instanceof Error ? err.message : String(err);
@@ -1456,7 +1482,8 @@ function normalizeResult3(raw) {
1456
1482
  answerText: parsed.answerText,
1457
1483
  citedDomains,
1458
1484
  groundingSources,
1459
- searchQueries
1485
+ searchQueries,
1486
+ retrievalStatus: useParsed ? parsed.retrievalStatus : raw.retrievalStatus
1460
1487
  };
1461
1488
  }
1462
1489
  function hasParsedResponseContent3(rawResponse) {
@@ -1475,6 +1502,7 @@ function reparseStoredResult3(rawResponse) {
1475
1502
  citedDomains: extractCitedDomainsFromSources3(groundingSources),
1476
1503
  groundingSources,
1477
1504
  searchQueries,
1505
+ retrievalStatus: extractRetrievalStatusFromRaw(rawResponse),
1478
1506
  ...providerErrors.length > 0 ? { providerError: `web_search tool error: ${providerErrors.join(", ")}` } : {}
1479
1507
  };
1480
1508
  }
@@ -1514,6 +1542,17 @@ function extractGroundingSourcesFromRaw2(rawResponse) {
1514
1542
  }
1515
1543
  return sources;
1516
1544
  }
1545
+ function extractRetrievalStatusFromRaw(rawResponse) {
1546
+ try {
1547
+ const content = rawResponse.content;
1548
+ if (!Array.isArray(content) || content.length === 0) return "unknown";
1549
+ return content.some(
1550
+ (block) => block.type === "server_tool_use" && block.name === "web_search"
1551
+ ) ? "used" : "not-used";
1552
+ } catch {
1553
+ return "unknown";
1554
+ }
1555
+ }
1517
1556
  function extractSearchQueriesFromRaw3(rawResponse) {
1518
1557
  const queries2 = /* @__PURE__ */ new Set();
1519
1558
  try {
@@ -1661,7 +1700,9 @@ var claudeAdapter = {
1661
1700
  model: raw.model,
1662
1701
  servedModel: raw.servedModel,
1663
1702
  groundingSources: raw.groundingSources,
1664
- searchQueries: raw.searchQueries
1703
+ searchQueries: raw.searchQueries,
1704
+ retrievalStatus: raw.retrievalStatus,
1705
+ retrievalContract: raw.retrievalContract
1665
1706
  };
1666
1707
  },
1667
1708
  normalizeResult(raw) {
@@ -1670,7 +1711,12 @@ var claudeAdapter = {
1670
1711
  rawResponse: raw.rawResponse,
1671
1712
  model: raw.model,
1672
1713
  groundingSources: raw.groundingSources,
1673
- searchQueries: raw.searchQueries
1714
+ searchQueries: raw.searchQueries,
1715
+ // The shared RawQueryResult now carries retrieval, so nothing is lost
1716
+ // across this boundary. A reconstruction that predates the field falls
1717
+ // back to `unknown` rather than asserting an absence.
1718
+ retrievalStatus: raw.retrievalStatus ?? "unknown",
1719
+ retrievalContract: raw.retrievalContract ?? CLAUDE_RETRIEVAL_CONTRACT
1674
1720
  };
1675
1721
  const normalized = normalizeResult3(claudeRaw);
1676
1722
  return {
@@ -1678,7 +1724,8 @@ var claudeAdapter = {
1678
1724
  answerText: normalized.answerText,
1679
1725
  citedDomains: normalized.citedDomains,
1680
1726
  groundingSources: normalized.groundingSources,
1681
- searchQueries: normalized.searchQueries
1727
+ searchQueries: normalized.searchQueries,
1728
+ retrievalStatus: normalized.retrievalStatus
1682
1729
  };
1683
1730
  },
1684
1731
  async generateText(prompt, config) {
@@ -1894,7 +1941,11 @@ var localAdapter = {
1894
1941
  model: raw.model,
1895
1942
  servedModel: raw.servedModel,
1896
1943
  groundingSources: raw.groundingSources,
1897
- searchQueries: raw.searchQueries
1944
+ searchQueries: raw.searchQueries,
1945
+ // A plain chat completion with no web access: there is no retrieval step
1946
+ // to report, which is what `not-applicable` means.
1947
+ retrievalStatus: "not-applicable",
1948
+ retrievalContract: "native-auto-v1"
1898
1949
  };
1899
1950
  },
1900
1951
  normalizeResult(raw) {
@@ -1911,7 +1962,8 @@ var localAdapter = {
1911
1962
  answerText: normalized.answerText,
1912
1963
  citedDomains: normalized.citedDomains,
1913
1964
  groundingSources: normalized.groundingSources,
1914
- searchQueries: normalized.searchQueries
1965
+ searchQueries: normalized.searchQueries,
1966
+ retrievalStatus: "not-applicable"
1915
1967
  };
1916
1968
  },
1917
1969
  async generateText(prompt, config) {
@@ -2360,7 +2412,8 @@ function normalizeResult5(raw) {
2360
2412
  answerText,
2361
2413
  citedDomains: extractCitedDomains(raw.groundingSources),
2362
2414
  groundingSources: raw.groundingSources,
2363
- searchQueries: raw.searchQueries
2415
+ searchQueries: raw.searchQueries,
2416
+ retrievalStatus: raw.retrievalStatus ?? "unknown"
2364
2417
  };
2365
2418
  }
2366
2419
 
@@ -2470,6 +2523,12 @@ var cdpChatgptAdapter = {
2470
2523
  model: "chatgpt-web",
2471
2524
  groundingSources,
2472
2525
  searchQueries: [input.query],
2526
+ // This surface is scraped from the rendered web UI, which exposes no
2527
+ // reliable signal for whether a retrieval step ran behind the answer.
2528
+ // `searchQueries` here is the query we submitted, not evidence of a
2529
+ // search the product performed.
2530
+ retrievalStatus: "unknown",
2531
+ retrievalContract: "native-auto-v1",
2473
2532
  screenshotPath: capturedScreenshotPath
2474
2533
  };
2475
2534
  } catch (err) {
@@ -2784,7 +2843,14 @@ var perplexityAdapter = {
2784
2843
  model: raw.model,
2785
2844
  servedModel: raw.servedModel,
2786
2845
  groundingSources: raw.groundingSources,
2787
- searchQueries: raw.searchQueries
2846
+ searchQueries: raw.searchQueries,
2847
+ // Retrieval detection is not implemented for this provider. Its candidate
2848
+ // marker is present on 100% of stored rows, so it has never been shown to
2849
+ // discriminate a non-retrieving answer and wiring it up would hardcode
2850
+ // `used`. `unknown` states what we actually know. The contract is a
2851
+ // declaration about how we build the request, so it is always knowable.
2852
+ retrievalStatus: "unknown",
2853
+ retrievalContract: "native-auto-v1"
2788
2854
  };
2789
2855
  },
2790
2856
  normalizeResult(raw) {
@@ -2801,7 +2867,8 @@ var perplexityAdapter = {
2801
2867
  answerText: normalized.answerText,
2802
2868
  citedDomains: normalized.citedDomains,
2803
2869
  groundingSources: normalized.groundingSources,
2804
- searchQueries: normalized.searchQueries
2870
+ searchQueries: normalized.searchQueries,
2871
+ retrievalStatus: "unknown"
2805
2872
  };
2806
2873
  },
2807
2874
  async generateText(prompt, config) {
@@ -3620,6 +3687,11 @@ var JobRunner = class {
3620
3687
  sourceCount: citedUrlCapture.sourceCount,
3621
3688
  resolvedCount: citedUrlCapture.resolvedCount,
3622
3689
  captureVersion: citedUrlCapture.captureVersion,
3690
+ // Retrieval is the adapter's own observation, never inferred
3691
+ // from citation counts. Both branches record it so no snapshot
3692
+ // can be written unmarked.
3693
+ retrievalStatus: normalized.retrievalStatus,
3694
+ retrievalContract: raw.retrievalContract,
3623
3695
  competitorOverlap: overlap,
3624
3696
  recommendedCompetitors: extractedCompetitors,
3625
3697
  location: runLocation?.label ?? null,
@@ -3651,6 +3723,11 @@ var JobRunner = class {
3651
3723
  sourceCount: citedUrlCapture.sourceCount,
3652
3724
  resolvedCount: citedUrlCapture.resolvedCount,
3653
3725
  captureVersion: citedUrlCapture.captureVersion,
3726
+ // Retrieval is the adapter's own observation, never inferred
3727
+ // from citation counts. Both branches record it so no snapshot
3728
+ // can be written unmarked.
3729
+ retrievalStatus: normalized.retrievalStatus,
3730
+ retrievalContract: raw.retrievalContract,
3654
3731
  competitorOverlap: overlap,
3655
3732
  recommendedCompetitors: extractedCompetitors,
3656
3733
  location: runLocation?.label ?? null,
@@ -6087,155 +6164,10 @@ function computeSummary2(rows) {
6087
6164
  return computeBacklinkSummaryMetrics(rows);
6088
6165
  }
6089
6166
 
6090
- // src/bing-backlinks-sync.ts
6167
+ // src/discovery-run.ts
6091
6168
  import crypto13 from "crypto";
6092
6169
  import { and as and9, eq as eq11 } from "drizzle-orm";
6093
- var log11 = createLogger("BingBacklinkSync");
6094
- var DEFAULT_MAX_PAGES = 200;
6095
- var LINK_COUNTS_MAX_PAGES = 50;
6096
- var URL_LINKS_MAX_PAGES = 20;
6097
- function bingReleaseId(date) {
6098
- return `bing-${date.toISOString().slice(0, 10)}`;
6099
- }
6100
- function aggregateInboundLinksByDomain(links, targetDomain) {
6101
- const target = hostOf(targetDomain);
6102
- const byHost = /* @__PURE__ */ new Map();
6103
- for (const link of links) {
6104
- const host = hostOf(link.Url);
6105
- if (!host) continue;
6106
- if (target && hostMatchesDomain(host, target)) continue;
6107
- let urls = byHost.get(host);
6108
- if (!urls) {
6109
- urls = /* @__PURE__ */ new Set();
6110
- byHost.set(host, urls);
6111
- }
6112
- urls.add(link.Url);
6113
- }
6114
- return [...byHost.entries()].map(([linkingDomain, urls]) => ({ linkingDomain, numHosts: urls.size })).sort((a, b) => b.numHosts - a.numHosts || a.linkingDomain.localeCompare(b.linkingDomain));
6115
- }
6116
- function computeBingSummary(rows) {
6117
- return computeBacklinkSummaryMetrics(rows);
6118
- }
6119
- function defaultDeps4() {
6120
- return { getLinkCounts, getUrlLinks, now: () => /* @__PURE__ */ new Date() };
6121
- }
6122
- async function executeBingBacklinkSync(db, runId, projectId, opts) {
6123
- const deps = { ...defaultDeps4(), ...opts.deps };
6124
- const startedAt = deps.now().toISOString();
6125
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, runId)).run();
6126
- try {
6127
- const project = db.select().from(projects).where(eq11(projects.id, projectId)).get();
6128
- if (!project) throw new Error(`Project not found: ${projectId}`);
6129
- const conn = opts.resolveConnection(project.canonicalDomain);
6130
- if (!conn) throw new Error(`No Bing Webmaster connection for ${project.canonicalDomain}`);
6131
- const siteUrl = conn.siteUrl;
6132
- if (!siteUrl) {
6133
- throw new Error(`Bing connection for ${project.canonicalDomain} has no verified site selected`);
6134
- }
6135
- const pages = await deps.getLinkCounts(conn.apiKey, siteUrl, { maxPages: LINK_COUNTS_MAX_PAGES });
6136
- const maxPages = Math.max(1, opts.maxPages ?? DEFAULT_MAX_PAGES);
6137
- const targetPages = [...pages].sort((a, b) => b.Count - a.Count).slice(0, maxPages);
6138
- if (pages.length > targetPages.length) {
6139
- log11.info("bing-sync.pages-capped", {
6140
- runId,
6141
- projectId,
6142
- sitePagesFound: pages.length,
6143
- sitePagesPulled: targetPages.length
6144
- });
6145
- }
6146
- const allLinks = [];
6147
- let pageFailures = 0;
6148
- for (const page of targetPages) {
6149
- try {
6150
- const links = await deps.getUrlLinks(conn.apiKey, siteUrl, page.Url, { maxPages: URL_LINKS_MAX_PAGES });
6151
- allLinks.push(...links);
6152
- } catch (err) {
6153
- pageFailures++;
6154
- log11.warn("bing-sync.page-failed", {
6155
- runId,
6156
- projectId,
6157
- page: page.Url,
6158
- error: err instanceof Error ? err.message : String(err)
6159
- });
6160
- }
6161
- }
6162
- if (targetPages.length > 0 && pageFailures === targetPages.length) {
6163
- throw new Error(`All ${targetPages.length} Bing inbound-link page fetch(es) failed`);
6164
- }
6165
- const rows = aggregateInboundLinksByDomain(allLinks, project.canonicalDomain);
6166
- const summary = computeBingSummary(rows);
6167
- const queriedAt = deps.now().toISOString();
6168
- const release = bingReleaseId(deps.now());
6169
- const targetDomain = project.canonicalDomain;
6170
- const source = BacklinkSources["bing-webmaster"];
6171
- const existing = db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and9(
6172
- eq11(backlinkSummaries.projectId, projectId),
6173
- eq11(backlinkSummaries.source, source),
6174
- eq11(backlinkSummaries.release, release)
6175
- )).get();
6176
- const preserveExisting = pageFailures > 0 && !!existing;
6177
- if (!preserveExisting) {
6178
- db.transaction((tx) => {
6179
- tx.delete(backlinkDomains).where(and9(
6180
- eq11(backlinkDomains.projectId, projectId),
6181
- eq11(backlinkDomains.source, source),
6182
- eq11(backlinkDomains.release, release)
6183
- )).run();
6184
- if (rows.length > 0) {
6185
- tx.insert(backlinkDomains).values(rows.map((r) => ({
6186
- id: crypto13.randomUUID(),
6187
- projectId,
6188
- releaseSyncId: null,
6189
- source,
6190
- release,
6191
- targetDomain,
6192
- linkingDomain: r.linkingDomain,
6193
- numHosts: r.numHosts,
6194
- createdAt: queriedAt
6195
- }))).run();
6196
- }
6197
- tx.insert(backlinkSummaries).values({
6198
- id: crypto13.randomUUID(),
6199
- projectId,
6200
- releaseSyncId: null,
6201
- source,
6202
- release,
6203
- targetDomain,
6204
- totalLinkingDomains: summary.totalLinkingDomains,
6205
- totalHosts: summary.totalHosts,
6206
- top10HostsShare: summary.top10HostsShare,
6207
- queriedAt,
6208
- createdAt: queriedAt
6209
- }).onConflictDoUpdate({
6210
- target: [backlinkSummaries.projectId, backlinkSummaries.source, backlinkSummaries.release],
6211
- set: {
6212
- targetDomain,
6213
- totalLinkingDomains: summary.totalLinkingDomains,
6214
- totalHosts: summary.totalHosts,
6215
- top10HostsShare: summary.top10HostsShare,
6216
- queriedAt
6217
- }
6218
- }).run();
6219
- });
6220
- }
6221
- const finishedAt = deps.now().toISOString();
6222
- const status = pageFailures > 0 ? RunStatuses.partial : RunStatuses.completed;
6223
- const error = preserveExisting ? `Kept existing ${release} snapshot; ${pageFailures} of ${targetPages.length} inbound-link page fetches failed` : pageFailures > 0 ? `${pageFailures} of ${targetPages.length} inbound-link page fetches failed` : null;
6224
- db.update(runs).set({ status, error, finishedAt }).where(eq11(runs.id, runId)).run();
6225
- log11.info("bing-sync.completed", { runId, projectId, release, rows: rows.length, status, pageFailures, preserveExisting });
6226
- } catch (err) {
6227
- const errorMsg = err instanceof Error ? err.message : String(err);
6228
- const finishedAt = deps.now().toISOString();
6229
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt }).where(eq11(runs.id, runId)).run();
6230
- log11.error("bing-sync.failed", { runId, projectId, error: errorMsg });
6231
- throw err;
6232
- }
6233
- }
6234
-
6235
- // src/discovery-run.ts
6236
- import crypto14 from "crypto";
6237
- import { and as and10, eq as eq12 } from "drizzle-orm";
6238
- var log12 = createLogger("DiscoveryRun");
6170
+ var log11 = createLogger("DiscoveryRun");
6239
6171
  var DEFAULT_SEED_COUNT = 30;
6240
6172
  var EMBED_RETRY_MAX_RETRIES = 3;
6241
6173
  var EMBED_RETRY_MAX_DELAY_MS = 1e4;
@@ -6268,7 +6200,7 @@ async function embedWithRetry(fn, opts = {}) {
6268
6200
  maxDelayMs: EMBED_RETRY_MAX_DELAY_MS,
6269
6201
  sleep: opts.sleep,
6270
6202
  onRetry: opts.onRetry ?? (({ attempt, delayMs }) => {
6271
- log12.warn("discovery.embed.retry", { attempt, delayMs });
6203
+ log11.warn("discovery.embed.retry", { attempt, delayMs });
6272
6204
  })
6273
6205
  }
6274
6206
  );
@@ -6276,11 +6208,11 @@ async function embedWithRetry(fn, opts = {}) {
6276
6208
  var QUERIES_PER_INTENT_BUCKET = 6;
6277
6209
  async function executeDiscoveryRun(opts) {
6278
6210
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6279
- opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq12(runs.id, opts.runId)).run();
6211
+ opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, opts.runId)).run();
6280
6212
  try {
6281
- const projectRow = opts.db.select().from(projects).where(eq12(projects.id, opts.projectId)).get();
6213
+ const projectRow = opts.db.select().from(projects).where(eq11(projects.id, opts.projectId)).get();
6282
6214
  if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
6283
- const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
6215
+ const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
6284
6216
  const canonicalDomains = effectiveDomains({
6285
6217
  canonicalDomain: projectRow.canonicalDomain,
6286
6218
  ownedDomains: projectRow.ownedDomains
@@ -6318,8 +6250,8 @@ async function executeDiscoveryRun(opts) {
6318
6250
  seedProvider: result.seedProvider,
6319
6251
  result
6320
6252
  });
6321
- opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, opts.runId)).run();
6322
- log12.info("discovery.completed", {
6253
+ opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq11(runs.id, opts.runId)).run();
6254
+ log11.info("discovery.completed", {
6323
6255
  runId: opts.runId,
6324
6256
  sessionId: opts.sessionId,
6325
6257
  buckets: result.buckets,
@@ -6327,13 +6259,13 @@ async function executeDiscoveryRun(opts) {
6327
6259
  });
6328
6260
  } catch (err) {
6329
6261
  const errorMsg = err instanceof Error ? err.message : String(err);
6330
- log12.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
6262
+ log11.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
6331
6263
  markSessionFailed(opts.db, opts.sessionId, errorMsg);
6332
6264
  opts.db.update(runs).set({
6333
6265
  status: RunStatuses.failed,
6334
6266
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
6335
6267
  error: errorMsg
6336
- }).where(eq12(runs.id, opts.runId)).run();
6268
+ }).where(eq11(runs.id, opts.runId)).run();
6337
6269
  }
6338
6270
  }
6339
6271
  function buildDefaultDeps(registry) {
@@ -6582,13 +6514,13 @@ function writeDiscoveryInsight(db, input) {
6582
6514
  totalProbes
6583
6515
  });
6584
6516
  db.transaction((tx) => {
6585
- tx.update(insights).set({ dismissed: true }).where(and10(
6586
- eq12(insights.projectId, input.projectId),
6587
- eq12(insights.type, "discovery.basket-divergence"),
6588
- eq12(insights.dismissed, false)
6517
+ tx.update(insights).set({ dismissed: true }).where(and9(
6518
+ eq11(insights.projectId, input.projectId),
6519
+ eq11(insights.type, "discovery.basket-divergence"),
6520
+ eq11(insights.dismissed, false)
6589
6521
  )).run();
6590
6522
  tx.insert(insights).values({
6591
- id: crypto14.randomUUID(),
6523
+ id: crypto13.randomUUID(),
6592
6524
  projectId: input.projectId,
6593
6525
  runId: input.runId,
6594
6526
  type: "discovery.basket-divergence",
@@ -6624,10 +6556,10 @@ function buildDiscoveryInsightTitle(input) {
6624
6556
  }
6625
6557
 
6626
6558
  // src/execute-site-audit.ts
6627
- import crypto15 from "crypto";
6628
- import { eq as eq13 } from "drizzle-orm";
6559
+ import crypto14 from "crypto";
6560
+ import { eq as eq12 } from "drizzle-orm";
6629
6561
  import { runSitemapAudit } from "@ainyc/aeo-audit";
6630
- var log13 = createLogger("SiteAudit");
6562
+ var log12 = createLogger("SiteAudit");
6631
6563
  var SITE_AUDIT_DEFAULT_PAGE_LIMIT = 500;
6632
6564
  var SITE_AUDIT_MAX_PAGE_LIMIT = 2e3;
6633
6565
  function toHomepageUrl(canonicalDomain) {
@@ -6688,15 +6620,15 @@ function computeFactorAverages(pages) {
6688
6620
  }
6689
6621
  async function executeSiteAudit(db, runId, projectId, opts = {}) {
6690
6622
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6691
- db.update(runs).set({ status: "running", startedAt }).where(eq13(runs.id, runId)).run();
6623
+ db.update(runs).set({ status: "running", startedAt }).where(eq12(runs.id, runId)).run();
6692
6624
  try {
6693
- const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
6625
+ const project = db.select().from(projects).where(eq12(projects.id, projectId)).get();
6694
6626
  if (!project) {
6695
6627
  throw new Error(`Project not found: ${projectId}`);
6696
6628
  }
6697
6629
  const homepageUrl = toHomepageUrl(project.canonicalDomain);
6698
6630
  const limit = clampSiteAuditLimit(opts.limit);
6699
- log13.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
6631
+ log12.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
6700
6632
  await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
6701
6633
  if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
6702
6634
  const report = await runSitemapAudit(homepageUrl, { sitemapUrl: opts.sitemapUrl, limit });
@@ -6704,7 +6636,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6704
6636
  const pagesErrored = report.pages.filter((page) => page.status === "error").length;
6705
6637
  const auditable = report.pagesDiscovered - report.pagesSkipped;
6706
6638
  if (auditable > report.pagesAudited) {
6707
- log13.info("truncated", {
6639
+ log12.info("truncated", {
6708
6640
  runId,
6709
6641
  projectId,
6710
6642
  auditable,
@@ -6723,7 +6655,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6723
6655
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
6724
6656
  db.transaction((tx) => {
6725
6657
  tx.insert(siteAuditSnapshots).values({
6726
- id: crypto15.randomUUID(),
6658
+ id: crypto14.randomUUID(),
6727
6659
  projectId,
6728
6660
  runId,
6729
6661
  sitemapUrl: report.sitemapUrl,
@@ -6752,7 +6684,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6752
6684
  }).run();
6753
6685
  for (const page of report.pages) {
6754
6686
  tx.insert(siteAuditPages).values({
6755
- id: crypto15.randomUUID(),
6687
+ id: crypto14.randomUUID(),
6756
6688
  projectId,
6757
6689
  runId,
6758
6690
  url: page.url,
@@ -6763,9 +6695,9 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6763
6695
  createdAt: finishedAt
6764
6696
  }).run();
6765
6697
  }
6766
- tx.update(runs).set({ status, finishedAt }).where(eq13(runs.id, runId)).run();
6698
+ tx.update(runs).set({ status, finishedAt }).where(eq12(runs.id, runId)).run();
6767
6699
  });
6768
- log13.info("completed", {
6700
+ log12.info("completed", {
6769
6701
  runId,
6770
6702
  projectId,
6771
6703
  status,
@@ -6775,14 +6707,14 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6775
6707
  });
6776
6708
  } catch (err) {
6777
6709
  const errorMsg = err instanceof Error ? err.message : String(err);
6778
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq13(runs.id, runId)).run();
6779
- log13.error("failed", { runId, projectId, error: errorMsg });
6710
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, runId)).run();
6711
+ log12.error("failed", { runId, projectId, error: errorMsg });
6780
6712
  throw err;
6781
6713
  }
6782
6714
  }
6783
6715
 
6784
6716
  // src/commands/backfill.ts
6785
- import { and as and11, eq as eq14, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
6717
+ import { and as and10, eq as eq13, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
6786
6718
  var SNAPSHOT_BATCH_SIZE = 500;
6787
6719
  async function backfillAnswerVisibilityCommand(opts) {
6788
6720
  const config = loadConfig();
@@ -6790,7 +6722,7 @@ async function backfillAnswerVisibilityCommand(opts) {
6790
6722
  migrate(db);
6791
6723
  const projectFilter = opts?.project?.trim();
6792
6724
  const isDryRun = opts?.dryRun === true;
6793
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq14(projects.name, projectFilter)).all() : db.select().from(projects).all();
6725
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
6794
6726
  let examined = 0;
6795
6727
  let updated = 0;
6796
6728
  let wouldUpdate = 0;
@@ -6798,10 +6730,10 @@ async function backfillAnswerVisibilityCommand(opts) {
6798
6730
  let reparsed = 0;
6799
6731
  let providerErrors = 0;
6800
6732
  if (scopedProjects.length > 0) {
6801
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and11(
6802
- eq14(runs.kind, RunKinds["answer-visibility"]),
6733
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and10(
6734
+ eq13(runs.kind, RunKinds["answer-visibility"]),
6803
6735
  inArray4(runs.projectId, scopedProjects.map((project) => project.id))
6804
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq14(runs.kind, RunKinds["answer-visibility"])).all();
6736
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq13(runs.kind, RunKinds["answer-visibility"])).all();
6805
6737
  const runIdsByProject = /* @__PURE__ */ new Map();
6806
6738
  for (const run of runRows) {
6807
6739
  const existing = runIdsByProject.get(run.projectId);
@@ -6809,7 +6741,7 @@ async function backfillAnswerVisibilityCommand(opts) {
6809
6741
  else runIdsByProject.set(run.projectId, [run.id]);
6810
6742
  }
6811
6743
  for (const project of scopedProjects) {
6812
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((row) => row.domain);
6744
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, project.id)).all().map((row) => row.domain);
6813
6745
  const runIds = runIdsByProject.get(project.id) ?? [];
6814
6746
  if (runIds.length === 0) continue;
6815
6747
  const projectDomains = effectiveDomains({
@@ -6855,7 +6787,12 @@ async function backfillAnswerVisibilityCommand(opts) {
6855
6787
  answerText,
6856
6788
  citedDomains: reparsedResult.citedDomains,
6857
6789
  groundingSources: reparsedResult.groundingSources,
6858
- searchQueries: reparsedResult.searchQueries
6790
+ searchQueries: reparsedResult.searchQueries,
6791
+ // Backfill recomputes mention and citation state from stored
6792
+ // rows; it never observes retrieval and never writes it back
6793
+ // (nextPatch carries no retrieval field). `unknown` keeps this
6794
+ // reconstruction from asserting anything about a search.
6795
+ retrievalStatus: "unknown"
6859
6796
  };
6860
6797
  const nextCitationState = determineCitationState(normalized, projectDomains);
6861
6798
  const nextCitedDomains = reparsedResult.citedDomains;
@@ -6897,7 +6834,7 @@ async function backfillAnswerVisibilityCommand(opts) {
6897
6834
  } else {
6898
6835
  db.transaction((tx) => {
6899
6836
  for (const update of pendingUpdates) {
6900
- tx.update(querySnapshots).set(update.patch).where(eq14(querySnapshots.id, update.id)).run();
6837
+ tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
6901
6838
  }
6902
6839
  });
6903
6840
  updated += pendingUpdates.length;
@@ -6946,13 +6883,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
6946
6883
  function backfillNormalizedPaths(db, opts) {
6947
6884
  const baseConditions = [];
6948
6885
  if (opts?.projectId) {
6949
- baseConditions.push(eq14(gaTrafficSnapshots.projectId, opts.projectId));
6886
+ baseConditions.push(eq13(gaTrafficSnapshots.projectId, opts.projectId));
6950
6887
  }
6951
6888
  const rows = db.select({
6952
6889
  id: gaTrafficSnapshots.id,
6953
6890
  landingPage: gaTrafficSnapshots.landingPage,
6954
6891
  landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
6955
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and11(...baseConditions) : void 0).all();
6892
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
6956
6893
  let updated = 0;
6957
6894
  let unchanged = 0;
6958
6895
  if (rows.length > 0) {
@@ -6967,7 +6904,7 @@ function backfillNormalizedPaths(db, opts) {
6967
6904
  unchanged++;
6968
6905
  continue;
6969
6906
  }
6970
- tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq14(gaTrafficSnapshots.id, row.id)).run();
6907
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq13(gaTrafficSnapshots.id, row.id)).run();
6971
6908
  updated++;
6972
6909
  }
6973
6910
  });
@@ -6981,7 +6918,7 @@ async function backfillNormalizedPathsCommand(opts) {
6981
6918
  const projectFilter = opts?.project?.trim();
6982
6919
  let projectId;
6983
6920
  if (projectFilter) {
6984
- const project = db.select({ id: projects.id }).from(projects).where(eq14(projects.name, projectFilter)).get();
6921
+ const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
6985
6922
  if (!project) {
6986
6923
  const result2 = {
6987
6924
  project: projectFilter,
@@ -7018,13 +6955,13 @@ async function backfillNormalizedPathsCommand(opts) {
7018
6955
  function backfillAiReferralPaths(db, opts) {
7019
6956
  const baseConditions = [];
7020
6957
  if (opts?.projectId) {
7021
- baseConditions.push(eq14(gaAiReferrals.projectId, opts.projectId));
6958
+ baseConditions.push(eq13(gaAiReferrals.projectId, opts.projectId));
7022
6959
  }
7023
6960
  const rows = db.select({
7024
6961
  id: gaAiReferrals.id,
7025
6962
  landingPage: gaAiReferrals.landingPage,
7026
6963
  landingPageNormalized: gaAiReferrals.landingPageNormalized
7027
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and11(...baseConditions) : void 0).all();
6964
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
7028
6965
  let updated = 0;
7029
6966
  let unchanged = 0;
7030
6967
  if (rows.length > 0) {
@@ -7039,7 +6976,7 @@ function backfillAiReferralPaths(db, opts) {
7039
6976
  unchanged++;
7040
6977
  continue;
7041
6978
  }
7042
- tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq14(gaAiReferrals.id, row.id)).run();
6979
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq13(gaAiReferrals.id, row.id)).run();
7043
6980
  updated++;
7044
6981
  }
7045
6982
  });
@@ -7053,7 +6990,7 @@ async function backfillAiReferralPathsCommand(opts) {
7053
6990
  const projectFilter = opts?.project?.trim();
7054
6991
  let projectId;
7055
6992
  if (projectFilter) {
7056
- const project = db.select({ id: projects.id }).from(projects).where(eq14(projects.name, projectFilter)).get();
6993
+ const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
7057
6994
  if (!project) {
7058
6995
  const result2 = {
7059
6996
  project: projectFilter,
@@ -7089,10 +7026,10 @@ async function backfillAiReferralPathsCommand(opts) {
7089
7026
  }
7090
7027
  function backfillProjectAnswerMentions(db, projectId, opts) {
7091
7028
  const isDryRun = opts?.dryRun === true;
7092
- const project = db.select().from(projects).where(eq14(projects.id, projectId)).get();
7029
+ const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
7093
7030
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
7094
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all().map((row) => row.domain);
7095
- const runRows = db.select({ id: runs.id }).from(runs).where(and11(eq14(runs.kind, RunKinds["answer-visibility"]), eq14(runs.projectId, projectId))).all();
7031
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all().map((row) => row.domain);
7032
+ const runRows = db.select({ id: runs.id }).from(runs).where(and10(eq13(runs.kind, RunKinds["answer-visibility"]), eq13(runs.projectId, projectId))).all();
7096
7033
  const runIds = runRows.map((r) => r.id);
7097
7034
  let examined = 0;
7098
7035
  let updated = 0;
@@ -7134,6 +7071,8 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
7134
7071
  answerText,
7135
7072
  citedDomains,
7136
7073
  groundingSources,
7074
+ // See the note above: retrieval is not observed or written here.
7075
+ retrievalStatus: "unknown",
7137
7076
  searchQueries: []
7138
7077
  };
7139
7078
  const nextCompetitorOverlap = computeCompetitorOverlap(normalized, competitorDomains);
@@ -7164,7 +7103,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
7164
7103
  } else {
7165
7104
  db.transaction((tx) => {
7166
7105
  for (const update of pendingUpdates) {
7167
- tx.update(querySnapshots).set(update.patch).where(eq14(querySnapshots.id, update.id)).run();
7106
+ tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
7168
7107
  }
7169
7108
  });
7170
7109
  updated += pendingUpdates.length;
@@ -7179,7 +7118,7 @@ async function backfillAnswerMentionsCommand(opts) {
7179
7118
  migrate(db);
7180
7119
  const projectFilter = opts?.project?.trim();
7181
7120
  const isDryRun = opts?.dryRun === true;
7182
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq14(projects.name, projectFilter)).all() : db.select().from(projects).all();
7121
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
7183
7122
  let examined = 0;
7184
7123
  let updated = 0;
7185
7124
  let wouldUpdate = 0;
@@ -7239,7 +7178,7 @@ function readStoredGroundingSources(rawResponse) {
7239
7178
  return result;
7240
7179
  }
7241
7180
  async function backfillInsightsCommand(project, opts) {
7242
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-5RJHJLYA.js");
7181
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-BGH2MGHB.js");
7243
7182
  const config = loadConfig();
7244
7183
  const db = createClient(config.database);
7245
7184
  migrate(db);
@@ -7398,7 +7337,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7398
7337
  const config = loadConfig();
7399
7338
  const db = createClient(config.database);
7400
7339
  migrate(db);
7401
- const project = db.select().from(projects).where(eq14(projects.name, opts.project)).get();
7340
+ const project = db.select().from(projects).where(eq13(projects.name, opts.project)).get();
7402
7341
  if (!project) {
7403
7342
  throw new Error(`Project "${opts.project}" not found`);
7404
7343
  }
@@ -7409,8 +7348,8 @@ async function backfillSnapshotAttributionCommand(opts) {
7409
7348
  process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
7410
7349
  `);
7411
7350
  }
7412
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and11(
7413
- eq14(auditLog.projectId, project.id),
7351
+ const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and10(
7352
+ eq13(auditLog.projectId, project.id),
7414
7353
  inArray4(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
7415
7354
  )).orderBy(auditLog.createdAt).all();
7416
7355
  const history = replayQueryAuditLog(events);
@@ -7418,8 +7357,8 @@ async function backfillSnapshotAttributionCommand(opts) {
7418
7357
  runId: runs.id,
7419
7358
  createdAt: runs.createdAt,
7420
7359
  location: runs.location
7421
- }).from(runs).innerJoin(querySnapshots, eq14(querySnapshots.runId, runs.id)).where(and11(
7422
- eq14(runs.projectId, project.id),
7360
+ }).from(runs).innerJoin(querySnapshots, eq13(querySnapshots.runId, runs.id)).where(and10(
7361
+ eq13(runs.projectId, project.id),
7423
7362
  isNull(querySnapshots.queryId),
7424
7363
  isNull(querySnapshots.queryText)
7425
7364
  )).groupBy(runs.id).orderBy(runs.createdAt).all();
@@ -7441,8 +7380,8 @@ async function backfillSnapshotAttributionCommand(opts) {
7441
7380
  provider: querySnapshots.provider,
7442
7381
  createdAt: querySnapshots.createdAt,
7443
7382
  answerText: querySnapshots.answerText
7444
- }).from(querySnapshots).where(and11(
7445
- eq14(querySnapshots.runId, run.runId),
7383
+ }).from(querySnapshots).where(and10(
7384
+ eq13(querySnapshots.runId, run.runId),
7446
7385
  isNull(querySnapshots.queryId),
7447
7386
  isNull(querySnapshots.queryText)
7448
7387
  )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
@@ -7508,7 +7447,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7508
7447
  if (!isDryRun && updates.length > 0) {
7509
7448
  db.transaction((tx) => {
7510
7449
  for (const u of updates) {
7511
- tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq14(querySnapshots.id, u.id)).run();
7450
+ tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq13(querySnapshots.id, u.id)).run();
7512
7451
  }
7513
7452
  });
7514
7453
  }
@@ -7582,7 +7521,7 @@ async function backfillTrafficClassificationCommand(opts) {
7582
7521
  const projectFilter = opts?.project?.trim();
7583
7522
  const isDryRun = opts?.dryRun === true;
7584
7523
  const isJson = isMachineFormat(opts?.format);
7585
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq14(projects.name, projectFilter)).all() : db.select().from(projects).all();
7524
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
7586
7525
  if (scopedProjects.length === 0) {
7587
7526
  if (projectFilter && !isJson) {
7588
7527
  process.stderr.write(`No project named "${projectFilter}".
@@ -7607,8 +7546,8 @@ async function backfillTrafficClassificationCommand(opts) {
7607
7546
  dryRun: isDryRun,
7608
7547
  byBot: {}
7609
7548
  };
7610
- const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and11(
7611
- eq14(rawEventSamples.eventType, "unknown"),
7549
+ const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
7550
+ eq13(rawEventSamples.eventType, "unknown"),
7612
7551
  inArray4(rawEventSamples.projectId, projectIds)
7613
7552
  )).get();
7614
7553
  result.unknownBefore = Number(unknownCountRow?.n ?? 0);
@@ -7620,8 +7559,8 @@ async function backfillTrafficClassificationCommand(opts) {
7620
7559
  userAgent: rawEventSamples.userAgent,
7621
7560
  pathNormalized: rawEventSamples.pathNormalized,
7622
7561
  status: rawEventSamples.status
7623
- }).from(rawEventSamples).where(and11(
7624
- eq14(rawEventSamples.eventType, "unknown"),
7562
+ }).from(rawEventSamples).where(and10(
7563
+ eq13(rawEventSamples.eventType, "unknown"),
7625
7564
  inArray4(rawEventSamples.projectId, projectIds)
7626
7565
  )).all();
7627
7566
  result.examined = unknownSamples.length;
@@ -7660,7 +7599,7 @@ async function backfillTrafficClassificationCommand(opts) {
7660
7599
  result.reclassified++;
7661
7600
  result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
7662
7601
  if (isDryRun) continue;
7663
- db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq14(rawEventSamples.id, snap.id)).run();
7602
+ db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq13(rawEventSamples.id, snap.id)).run();
7664
7603
  const tsHour = new Date(snap.ts);
7665
7604
  tsHour.setUTCMinutes(0, 0, 0);
7666
7605
  if (userFetch) {
@@ -7724,8 +7663,8 @@ async function backfillTrafficClassificationCommand(opts) {
7724
7663
  }
7725
7664
  }
7726
7665
  if (!isDryRun) {
7727
- const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and11(
7728
- eq14(rawEventSamples.eventType, "unknown"),
7666
+ const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
7667
+ eq13(rawEventSamples.eventType, "unknown"),
7729
7668
  inArray4(rawEventSamples.projectId, projectIds)
7730
7669
  )).get();
7731
7670
  result.unknownAfter = Number(afterRow?.n ?? 0);
@@ -7760,7 +7699,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
7760
7699
  }
7761
7700
 
7762
7701
  // src/commands/skills.ts
7763
- import crypto16 from "crypto";
7702
+ import crypto15 from "crypto";
7764
7703
  import fs4 from "fs";
7765
7704
  import os4 from "os";
7766
7705
  import path5 from "path";
@@ -7815,7 +7754,7 @@ function walkRelative(dir, prefix = "") {
7815
7754
  return out.sort();
7816
7755
  }
7817
7756
  function sha256File(filePath) {
7818
- return crypto16.createHash("sha256").update(fs4.readFileSync(filePath)).digest("hex");
7757
+ return crypto15.createHash("sha256").update(fs4.readFileSync(filePath)).digest("hex");
7819
7758
  }
7820
7759
  function readSkillManifest(skillDir) {
7821
7760
  try {
@@ -8157,10 +8096,11 @@ var ProviderRegistry = class {
8157
8096
  };
8158
8097
 
8159
8098
  // src/scheduler.ts
8160
- import crypto17 from "crypto";
8099
+ import crypto16 from "crypto";
8161
8100
  import cron from "node-cron";
8162
- import { and as and12, eq as eq15, inArray as inArray5 } from "drizzle-orm";
8163
- var log14 = createLogger("Scheduler");
8101
+ import { and as and11, eq as eq14, inArray as inArray5, notExists, sql as sql6 } from "drizzle-orm";
8102
+ var log13 = createLogger("Scheduler");
8103
+ var DEFAULT_HEALTH_CRON = "0 */6 * * *";
8164
8104
  function taskKey(projectId, kind) {
8165
8105
  return `${projectId}::${kind}`;
8166
8106
  }
@@ -8172,18 +8112,87 @@ var Scheduler = class {
8172
8112
  this.db = db;
8173
8113
  this.callbacks = callbacks;
8174
8114
  }
8115
+ /**
8116
+ * Give every project a health schedule if it has never had one.
8117
+ *
8118
+ * Ships the schedule with the feature. The six checks that existed before
8119
+ * this had never run on a live instance because nothing scheduled them, and
8120
+ * a health check nobody enables is indistinguishable from no health check.
8121
+ *
8122
+ * Keyed on the row EXISTING, not on it being enabled, so an operator who
8123
+ * deliberately turns this off keeps it off across restarts. Seeded here
8124
+ * rather than in a migration because migrations must stay additive to remain
8125
+ * downgrade-safe, and inserting rows is a data mutation.
8126
+ *
8127
+ * Every 6h, not daily: the cadence has to be shorter than the time-to-damage
8128
+ * it guards. A Vercel source begins discarding traffic once it is 24h behind,
8129
+ * so a daily pass could only ever observe the loss after it started.
8130
+ */
8131
+ ensureHealthSchedules() {
8132
+ const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
8133
+ this.db.select({ one: sql6`1` }).from(schedules).where(and11(
8134
+ eq14(schedules.projectId, projects.id),
8135
+ eq14(schedules.kind, SchedulableRunKinds.doctor)
8136
+ ))
8137
+ )).all();
8138
+ if (projectsWithoutHealth.length === 0) return;
8139
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8140
+ for (const project of projectsWithoutHealth) {
8141
+ this.db.insert(schedules).values({
8142
+ id: crypto16.randomUUID(),
8143
+ projectId: project.id,
8144
+ kind: SchedulableRunKinds.doctor,
8145
+ cronExpr: DEFAULT_HEALTH_CRON,
8146
+ timezone: "UTC",
8147
+ enabled: true,
8148
+ providers: [],
8149
+ nextRunAt: nextRunFromCron(DEFAULT_HEALTH_CRON, "UTC"),
8150
+ createdAt: now,
8151
+ updatedAt: now
8152
+ }).run();
8153
+ }
8154
+ log13.info("health-schedule.seeded", { projectCount: projectsWithoutHealth.length, cron: DEFAULT_HEALTH_CRON });
8155
+ }
8156
+ /**
8157
+ * Record each project's current query set as basket revision 1 if it has
8158
+ * never been recorded. Minting normally happens when a sweep is queued, but a
8159
+ * project whose sweeps are manual (the common cadence: twice a month) would
8160
+ * otherwise keep its analytics on the pre-basket date heuristic for weeks
8161
+ * after the feature ships — with the chart still hiding exactly the history
8162
+ * the basket exists to recover. Boot is the moment the current set is known
8163
+ * and nothing has to be guessed, and ensureCurrentQueryBasketRevision is
8164
+ * idempotent, so restarts are no-ops and an unchanged set never churns
8165
+ * revisions. Runs are NOT stamped retroactively: the revision describes the
8166
+ * set as of now, and historical runs keep their null stamp.
8167
+ */
8168
+ ensureQueryBaskets() {
8169
+ const allProjects = this.db.select({ id: projects.id }).from(projects).all();
8170
+ let minted = 0;
8171
+ for (const project of allProjects) {
8172
+ try {
8173
+ const previous = latestQueryBasketRevision(this.db, project.id)?.revision ?? null;
8174
+ const current = ensureCurrentQueryBasketRevision(this.db, project.id);
8175
+ if (current !== null && current.revision !== previous) minted += 1;
8176
+ } catch (err) {
8177
+ log13.warn("query-basket.mint-failed", { projectId: project.id, err: String(err) });
8178
+ }
8179
+ }
8180
+ if (minted > 0) log13.info("query-basket.ensured", { projectCount: minted });
8181
+ }
8175
8182
  /** Load all enabled schedules from DB and register cron jobs. */
8176
8183
  start() {
8177
- const allSchedules = this.db.select().from(schedules).where(eq15(schedules.enabled, true)).all();
8184
+ this.ensureHealthSchedules();
8185
+ this.ensureQueryBaskets();
8186
+ const allSchedules = this.db.select().from(schedules).where(eq14(schedules.enabled, true)).all();
8178
8187
  for (const schedule of allSchedules) {
8179
8188
  const missedRunAt = schedule.nextRunAt;
8180
8189
  this.registerCronTask(schedule);
8181
8190
  if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
8182
- log14.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
8191
+ log13.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
8183
8192
  this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
8184
8193
  }
8185
8194
  }
8186
- log14.info("started", { scheduleCount: allSchedules.length });
8195
+ log13.info("started", { scheduleCount: allSchedules.length });
8187
8196
  }
8188
8197
  /** Stop all cron tasks for graceful shutdown. */
8189
8198
  stop() {
@@ -8204,7 +8213,7 @@ var Scheduler = class {
8204
8213
  this.stopTask(key, existing, "Stopped");
8205
8214
  this.tasks.delete(key);
8206
8215
  }
8207
- const schedule = this.db.select().from(schedules).where(and12(eq15(schedules.projectId, projectId), eq15(schedules.kind, kind))).get();
8216
+ const schedule = this.db.select().from(schedules).where(and11(eq14(schedules.projectId, projectId), eq14(schedules.kind, kind))).get();
8208
8217
  if (schedule && schedule.enabled) {
8209
8218
  this.registerCronTask(schedule);
8210
8219
  }
@@ -8227,13 +8236,13 @@ var Scheduler = class {
8227
8236
  stopTask(key, task, verb) {
8228
8237
  void task.stop();
8229
8238
  void task.destroy();
8230
- log14.info(`task.${verb.toLowerCase()}`, { key });
8239
+ log13.info(`task.${verb.toLowerCase()}`, { key });
8231
8240
  }
8232
8241
  registerCronTask(schedule) {
8233
8242
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
8234
8243
  const kind = schedule.kind;
8235
8244
  if (!cron.validate(cronExpr)) {
8236
- log14.error("cron.invalid", { projectId, kind, cronExpr });
8245
+ log13.error("cron.invalid", { projectId, kind, cronExpr });
8237
8246
  return;
8238
8247
  }
8239
8248
  const task = cron.schedule(cronExpr, () => {
@@ -8245,51 +8254,51 @@ var Scheduler = class {
8245
8254
  this.db.update(schedules).set({
8246
8255
  nextRunAt: nextRunFromCron(cronExpr, timezone),
8247
8256
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8248
- }).where(eq15(schedules.id, scheduleId)).run();
8257
+ }).where(eq14(schedules.id, scheduleId)).run();
8249
8258
  const label = schedule.preset ?? cronExpr;
8250
- log14.info("cron.registered", { projectId, kind, schedule: label, timezone });
8259
+ log13.info("cron.registered", { projectId, kind, schedule: label, timezone });
8251
8260
  }
8252
8261
  triggerRun(scheduleId, projectId, kind) {
8253
8262
  try {
8254
8263
  const now = (/* @__PURE__ */ new Date()).toISOString();
8255
- const currentSchedule = this.db.select().from(schedules).where(eq15(schedules.id, scheduleId)).get();
8264
+ const currentSchedule = this.db.select().from(schedules).where(eq14(schedules.id, scheduleId)).get();
8256
8265
  if (!currentSchedule || !currentSchedule.enabled) {
8257
- log14.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
8266
+ log13.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
8258
8267
  this.remove(projectId, kind);
8259
8268
  return;
8260
8269
  }
8261
8270
  const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
8262
- const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8271
+ const project = this.db.select().from(projects).where(eq14(projects.id, projectId)).get();
8263
8272
  if (!project) {
8264
- log14.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
8273
+ log13.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
8265
8274
  this.remove(projectId, kind);
8266
8275
  return;
8267
8276
  }
8268
8277
  if (kind === SchedulableRunKinds["traffic-sync"]) {
8269
8278
  const sourceId = currentSchedule.sourceId;
8270
8279
  if (!sourceId) {
8271
- log14.warn("traffic-sync.missing-source", { scheduleId, projectId });
8280
+ log13.warn("traffic-sync.missing-source", { scheduleId, projectId });
8272
8281
  return;
8273
8282
  }
8274
8283
  if (!this.callbacks.onTrafficSyncRequested) {
8275
- log14.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
8284
+ log13.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
8276
8285
  return;
8277
8286
  }
8278
8287
  this.db.update(schedules).set({
8279
8288
  lastRunAt: now,
8280
8289
  nextRunAt,
8281
8290
  updatedAt: now
8282
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8283
- log14.info("traffic-sync.triggered", { projectName: project.name, sourceId });
8291
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8292
+ log13.info("traffic-sync.triggered", { projectName: project.name, sourceId });
8284
8293
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
8285
8294
  return;
8286
8295
  }
8287
8296
  if (kind === SchedulableRunKinds["gbp-sync"]) {
8288
8297
  if (!this.callbacks.onGbpSyncRequested) {
8289
- log14.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
8298
+ log13.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
8290
8299
  return;
8291
8300
  }
8292
- const runId2 = crypto17.randomUUID();
8301
+ const runId2 = crypto16.randomUUID();
8293
8302
  this.db.insert(runs).values({
8294
8303
  id: runId2,
8295
8304
  projectId,
@@ -8302,27 +8311,27 @@ var Scheduler = class {
8302
8311
  lastRunAt: now,
8303
8312
  nextRunAt,
8304
8313
  updatedAt: now
8305
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8306
- log14.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
8314
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8315
+ log13.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
8307
8316
  this.callbacks.onGbpSyncRequested(runId2, projectId);
8308
8317
  return;
8309
8318
  }
8310
8319
  if (kind === SchedulableRunKinds["ads-sync"]) {
8311
8320
  if (!this.callbacks.onAdsSyncRequested) {
8312
- log14.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
8321
+ log13.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
8313
8322
  return;
8314
8323
  }
8315
- const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and12(
8316
- eq15(runs.projectId, projectId),
8317
- eq15(runs.kind, RunKinds["ads-sync"]),
8324
+ const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and11(
8325
+ eq14(runs.projectId, projectId),
8326
+ eq14(runs.kind, RunKinds["ads-sync"]),
8318
8327
  inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
8319
8328
  )).get();
8320
8329
  if (activeAdsRun) {
8321
- log14.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
8322
- this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq15(schedules.id, currentSchedule.id)).run();
8330
+ log13.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
8331
+ this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
8323
8332
  return;
8324
8333
  }
8325
- const runId2 = crypto17.randomUUID();
8334
+ const runId2 = crypto16.randomUUID();
8326
8335
  this.db.insert(runs).values({
8327
8336
  id: runId2,
8328
8337
  projectId,
@@ -8335,55 +8344,69 @@ var Scheduler = class {
8335
8344
  lastRunAt: now,
8336
8345
  nextRunAt,
8337
8346
  updatedAt: now
8338
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8339
- log14.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
8347
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8348
+ log13.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
8340
8349
  this.callbacks.onAdsSyncRequested(runId2, projectId);
8341
8350
  return;
8342
8351
  }
8343
8352
  if (kind === SchedulableRunKinds["data-refresh"]) {
8344
8353
  if (!this.callbacks.onDataRefreshRequested) {
8345
- log14.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
8354
+ log13.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
8346
8355
  return;
8347
8356
  }
8348
8357
  this.db.update(schedules).set({
8349
8358
  lastRunAt: now,
8350
8359
  nextRunAt,
8351
8360
  updatedAt: now
8352
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8353
- log14.info("data-refresh.triggered", { projectName: project.name });
8361
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8362
+ log13.info("data-refresh.triggered", { projectName: project.name });
8354
8363
  this.callbacks.onDataRefreshRequested(project.name);
8355
8364
  return;
8356
8365
  }
8366
+ if (kind === SchedulableRunKinds.doctor) {
8367
+ if (!this.callbacks.onDoctorRequested) {
8368
+ log13.warn("doctor.no-callback", { scheduleId, projectId, msg: "host did not register onDoctorRequested" });
8369
+ return;
8370
+ }
8371
+ this.db.update(schedules).set({
8372
+ lastRunAt: now,
8373
+ nextRunAt,
8374
+ updatedAt: now
8375
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8376
+ log13.info("doctor.triggered", { projectName: project.name });
8377
+ this.callbacks.onDoctorRequested(project.name);
8378
+ return;
8379
+ }
8357
8380
  if (kind === SchedulableRunKinds["backlinks-sync"]) {
8358
8381
  if (!this.callbacks.onBacklinksSyncRequested) {
8359
- log14.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
8382
+ log13.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
8360
8383
  return;
8361
8384
  }
8362
8385
  this.db.update(schedules).set({
8363
8386
  lastRunAt: now,
8364
8387
  nextRunAt,
8365
8388
  updatedAt: now
8366
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8367
- log14.info("backlinks-sync.triggered", { projectName: project.name });
8389
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8390
+ log13.info("backlinks-sync.triggered", { projectName: project.name });
8368
8391
  this.callbacks.onBacklinksSyncRequested(project.name);
8369
8392
  return;
8370
8393
  }
8371
8394
  if (kind === SchedulableRunKinds["site-audit"]) {
8372
8395
  if (!this.callbacks.onSiteAuditRequested) {
8373
- log14.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
8396
+ log13.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
8374
8397
  return;
8375
8398
  }
8376
- const active = this.db.select({ id: runs.id }).from(runs).where(and12(
8377
- eq15(runs.projectId, projectId),
8378
- eq15(runs.kind, RunKinds["site-audit"]),
8399
+ const active = this.db.select({ id: runs.id }).from(runs).where(and11(
8400
+ eq14(runs.projectId, projectId),
8401
+ eq14(runs.kind, RunKinds["site-audit"]),
8379
8402
  inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
8380
8403
  )).get();
8381
8404
  if (active) {
8382
- log14.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
8383
- this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq15(schedules.id, currentSchedule.id)).run();
8405
+ log13.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
8406
+ this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
8384
8407
  return;
8385
8408
  }
8386
- const runId2 = crypto17.randomUUID();
8409
+ const runId2 = crypto16.randomUUID();
8387
8410
  this.db.insert(runs).values({
8388
8411
  id: runId2,
8389
8412
  projectId,
@@ -8396,8 +8419,8 @@ var Scheduler = class {
8396
8419
  lastRunAt: now,
8397
8420
  nextRunAt,
8398
8421
  updatedAt: now
8399
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8400
- log14.info("site-audit.triggered", { runId: runId2, projectName: project.name });
8422
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8423
+ log13.info("site-audit.triggered", { runId: runId2, projectName: project.name });
8401
8424
  this.callbacks.onSiteAuditRequested(runId2, projectId);
8402
8425
  return;
8403
8426
  }
@@ -8406,7 +8429,7 @@ var Scheduler = class {
8406
8429
  if (project.defaultLocation) {
8407
8430
  const loc = projectLocations.find((l) => l.label === project.defaultLocation);
8408
8431
  if (!loc) {
8409
- log14.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
8432
+ log13.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
8410
8433
  return;
8411
8434
  }
8412
8435
  resolvedLocation = loc;
@@ -8420,11 +8443,11 @@ var Scheduler = class {
8420
8443
  location: locationLabel
8421
8444
  });
8422
8445
  if (queueResult.conflict) {
8423
- log14.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
8446
+ log13.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
8424
8447
  this.db.update(schedules).set({
8425
8448
  nextRunAt,
8426
8449
  updatedAt: now
8427
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8450
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8428
8451
  return;
8429
8452
  }
8430
8453
  const runId = queueResult.runId;
@@ -8432,19 +8455,19 @@ var Scheduler = class {
8432
8455
  lastRunAt: now,
8433
8456
  nextRunAt,
8434
8457
  updatedAt: now
8435
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8458
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8436
8459
  const scheduleProviders = currentSchedule.providers;
8437
8460
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
8438
- log14.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
8461
+ log13.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
8439
8462
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
8440
8463
  } catch (err) {
8441
- log14.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
8464
+ log13.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
8442
8465
  }
8443
8466
  }
8444
8467
  };
8445
8468
 
8446
8469
  // src/data-refresh.ts
8447
- var log15 = createLogger("DataRefresh");
8470
+ var log14 = createLogger("DataRefresh");
8448
8471
  async function refreshAllIntegrations(client, projectName) {
8449
8472
  const integrations = [
8450
8473
  { name: "gsc", run: () => client.gscSync(projectName, {}) },
@@ -8457,19 +8480,19 @@ async function refreshAllIntegrations(client, projectName) {
8457
8480
  results.forEach((result, idx) => {
8458
8481
  const integration = integrations[idx].name;
8459
8482
  if (result.status === "fulfilled") {
8460
- log15.info("integration.refreshed", { projectName, integration });
8483
+ log14.info("integration.refreshed", { projectName, integration });
8461
8484
  } else {
8462
8485
  const reason = result.reason;
8463
8486
  const message = reason instanceof Error ? reason.message : String(reason);
8464
- log15.warn("integration.refresh-failed", { projectName, integration, error: message });
8487
+ log14.warn("integration.refresh-failed", { projectName, integration, error: message });
8465
8488
  }
8466
8489
  });
8467
8490
  }
8468
8491
 
8469
8492
  // src/notifier.ts
8470
- import { eq as eq16, desc as desc5, and as and13, inArray as inArray6, or } from "drizzle-orm";
8471
- import crypto18 from "crypto";
8472
- var log16 = createLogger("Notifier");
8493
+ import { eq as eq15, desc as desc5, and as and12, inArray as inArray6, or } from "drizzle-orm";
8494
+ import crypto17 from "crypto";
8495
+ var log15 = createLogger("Notifier");
8473
8496
  var Notifier = class {
8474
8497
  db;
8475
8498
  serverUrl;
@@ -8479,26 +8502,26 @@ var Notifier = class {
8479
8502
  }
8480
8503
  /** Called after a run completes (success, partial, or failed). */
8481
8504
  async onRunCompleted(runId, projectId) {
8482
- log16.info("run.completed", { runId, projectId });
8483
- const notifs = this.db.select().from(notifications).where(eq16(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8505
+ log15.info("run.completed", { runId, projectId });
8506
+ const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8484
8507
  if (notifs.length === 0) {
8485
- log16.info("notifications.none-enabled", { projectId });
8508
+ log15.info("notifications.none-enabled", { projectId });
8486
8509
  return;
8487
8510
  }
8488
- log16.info("notifications.found", { projectId, count: notifs.length });
8489
- const run = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8511
+ log15.info("notifications.found", { projectId, count: notifs.length });
8512
+ const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8490
8513
  if (!run) {
8491
- log16.error("run.not-found", { runId, msg: "skipping notification dispatch" });
8514
+ log15.error("run.not-found", { runId, msg: "skipping notification dispatch" });
8492
8515
  return;
8493
8516
  }
8494
- const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
8517
+ const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8495
8518
  if (!project) {
8496
- log16.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
8519
+ log15.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
8497
8520
  return;
8498
8521
  }
8499
8522
  const transitions = this.computeTransitions(runId, projectId);
8500
8523
  const events = [];
8501
- log16.info("run.status", { runId: run.id, status: run.status, projectId });
8524
+ log15.info("run.status", { runId: run.id, status: run.status, projectId });
8502
8525
  if (run.status === "completed" || run.status === "partial") {
8503
8526
  events.push("run.completed");
8504
8527
  }
@@ -8514,7 +8537,7 @@ var Notifier = class {
8514
8537
  if (!config.url) continue;
8515
8538
  const subscribedEvents = config.events;
8516
8539
  const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
8517
- log16.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
8540
+ log15.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
8518
8541
  if (matchingEvents.length === 0) continue;
8519
8542
  for (const event of matchingEvents) {
8520
8543
  const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
@@ -8530,6 +8553,101 @@ var Notifier = class {
8530
8553
  }
8531
8554
  }
8532
8555
  }
8556
+ /**
8557
+ * Record the outcome of a scheduled health pass and notify on transitions.
8558
+ *
8559
+ * Edge-triggered on `(status, code)`. A pass that finds the same problem as
8560
+ * last time is stored but not re-sent: an operator who receives the same
8561
+ * warning every day stops reading the channel, which is how a real
8562
+ * degradation goes unnoticed. A pass whose worst code CHANGES does notify,
8563
+ * even at the same severity, because a different cause is different news.
8564
+ *
8565
+ * Returns the event emitted, or null when nothing changed — the return value
8566
+ * is what tests assert against, so "stayed quiet" is observable rather than
8567
+ * inferred from the absence of a webhook.
8568
+ */
8569
+ async onHealthChecked(projectId, report) {
8570
+ const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8571
+ if (!project) {
8572
+ log15.error("project.not-found", { projectId, msg: "skipping health notification" });
8573
+ return null;
8574
+ }
8575
+ const rank = (status2) => status2 === "fail" ? 2 : status2 === "warn" ? 1 : 0;
8576
+ const categoryRank = (category) => {
8577
+ switch (category) {
8578
+ case "database":
8579
+ return 6;
8580
+ case "integrations":
8581
+ return 5;
8582
+ case "providers":
8583
+ return 4;
8584
+ case "auth":
8585
+ return 3;
8586
+ case "schedules":
8587
+ return 2;
8588
+ case "config":
8589
+ return 1;
8590
+ default:
8591
+ return 0;
8592
+ }
8593
+ };
8594
+ const graded = report.checks.filter((c) => c.status === "fail" || c.status === "warn" || c.status === "ok");
8595
+ const failing = graded.filter((c) => c.status !== "ok").sort((a, b) => rank(b.status) - rank(a.status) || categoryRank(b.category) - categoryRank(a.category) || a.id.localeCompare(b.id));
8596
+ const worst = failing.at(0) ?? null;
8597
+ const noSignal = graded.length === 0;
8598
+ const status = noSignal ? "warn" : worst ? worst.status : "ok";
8599
+ const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
8600
+ 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.`;
8601
+ const previous = this.db.select().from(doctorHealthState).where(eq15(doctorHealthState.projectId, projectId)).get();
8602
+ const previousStatus = previous?.status ?? null;
8603
+ let event = null;
8604
+ if (previous === void 0) {
8605
+ if (status !== "ok") event = "health.degraded";
8606
+ } else if (status === "ok" && previousStatus !== "ok") {
8607
+ event = "health.recovered";
8608
+ } else if (status !== "ok" && (previousStatus === "ok" || previous.code !== code)) {
8609
+ event = "health.degraded";
8610
+ }
8611
+ const now = report.checkedAt;
8612
+ const observation = { projectId, status, code, summary, checkedAt: now };
8613
+ if (previous === void 0) {
8614
+ this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
8615
+ } else {
8616
+ this.db.update(doctorHealthState).set(observation).where(eq15(doctorHealthState.projectId, projectId)).run();
8617
+ }
8618
+ if (!event) {
8619
+ log15.info("health.unchanged", { projectId, status, code });
8620
+ return null;
8621
+ }
8622
+ const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8623
+ const payload = {
8624
+ source: "canonry",
8625
+ event,
8626
+ project: { name: project.name, canonicalDomain: project.canonicalDomain },
8627
+ health: {
8628
+ status,
8629
+ code,
8630
+ summary,
8631
+ remediation: worst?.remediation ?? null,
8632
+ checkedAt: now,
8633
+ previousStatus,
8634
+ failing: failing.map((c) => ({ id: c.id, status: c.status, code: c.code, summary: c.summary }))
8635
+ },
8636
+ dashboardUrl: `${this.serverUrl}/projects/${project.name}`
8637
+ };
8638
+ let delivered = 0;
8639
+ for (const notif of notifs) {
8640
+ const config = notif.config;
8641
+ if (!config.url) continue;
8642
+ await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
8643
+ delivered += 1;
8644
+ }
8645
+ if (delivered > 0) {
8646
+ this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq15(doctorHealthState.projectId, projectId)).run();
8647
+ }
8648
+ log15.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
8649
+ return event;
8650
+ }
8533
8651
  /** Dispatch insight webhooks for critical/high severity insights after a run. */
8534
8652
  async dispatchInsightWebhooks(runId, projectId, result) {
8535
8653
  const insightEvents = [];
@@ -8538,11 +8656,11 @@ var Notifier = class {
8538
8656
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
8539
8657
  if (highInsights.length > 0) insightEvents.push("insight.high");
8540
8658
  if (insightEvents.length === 0) return;
8541
- const notifs = this.db.select().from(notifications).where(eq16(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8659
+ const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8542
8660
  if (notifs.length === 0) return;
8543
- const run = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8661
+ const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8544
8662
  if (!run) return;
8545
- const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
8663
+ const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8546
8664
  if (!project) return;
8547
8665
  for (const notif of notifs) {
8548
8666
  const config = notif.config;
@@ -8572,12 +8690,12 @@ var Notifier = class {
8572
8690
  }
8573
8691
  }
8574
8692
  computeTransitions(runId, projectId) {
8575
- const thisRun = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8693
+ const thisRun = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8576
8694
  if (!thisRun) return [];
8577
- const groupSiblings = this.db.select().from(runs).where(and13(
8578
- eq16(runs.projectId, projectId),
8579
- eq16(runs.kind, thisRun.kind),
8580
- eq16(runs.createdAt, thisRun.createdAt)
8695
+ const groupSiblings = this.db.select().from(runs).where(and12(
8696
+ eq15(runs.projectId, projectId),
8697
+ eq15(runs.kind, thisRun.kind),
8698
+ eq15(runs.createdAt, thisRun.createdAt)
8581
8699
  )).all();
8582
8700
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
8583
8701
  if (stillPending) return [];
@@ -8593,17 +8711,17 @@ var Notifier = class {
8593
8711
  return candidate.id > best.id ? candidate : best;
8594
8712
  });
8595
8713
  if (winner.id !== runId) return [];
8596
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq16(projects.id, projectId)).get();
8714
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq15(projects.id, projectId)).get();
8597
8715
  const locationCount = Math.max(
8598
8716
  1,
8599
8717
  (projectLocations?.locations ?? []).length
8600
8718
  );
8601
8719
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
8602
8720
  const recentRuns = this.db.select().from(runs).where(
8603
- and13(
8604
- eq16(runs.projectId, projectId),
8605
- eq16(runs.kind, thisRun.kind),
8606
- or(eq16(runs.status, "completed"), eq16(runs.status, "partial"))
8721
+ and12(
8722
+ eq15(runs.projectId, projectId),
8723
+ eq15(runs.kind, thisRun.kind),
8724
+ or(eq15(runs.status, "completed"), eq15(runs.status, "partial"))
8607
8725
  )
8608
8726
  ).orderBy(desc5(runs.createdAt), desc5(runs.id)).limit(RECENT_FETCH_LIMIT).all();
8609
8727
  const groups = groupRunsByCreatedAt(recentRuns);
@@ -8620,7 +8738,7 @@ var Notifier = class {
8620
8738
  provider: querySnapshots.provider,
8621
8739
  location: querySnapshots.location,
8622
8740
  citationState: querySnapshots.citationState
8623
- }).from(querySnapshots).leftJoin(queries, eq16(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
8741
+ }).from(querySnapshots).leftJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
8624
8742
  const previousSnapshots = this.db.select({
8625
8743
  queryId: querySnapshots.queryId,
8626
8744
  provider: querySnapshots.provider,
@@ -8650,26 +8768,29 @@ var Notifier = class {
8650
8768
  return transitions;
8651
8769
  }
8652
8770
  async sendWebhook(url, payload, notificationId, projectId, webhookSecret) {
8771
+ const destination = resolveDestination(url);
8772
+ const body = destination.render ? destination.render(toAlertView(payload)) : payload;
8773
+ const signingSecret = destination.signed ? webhookSecret : null;
8653
8774
  const targetLabel = redactNotificationUrl(url).urlDisplay;
8654
8775
  const targetCheck = await resolveWebhookTarget(url);
8655
8776
  if (!targetCheck.ok) {
8656
- log16.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
8777
+ log15.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
8657
8778
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
8658
8779
  return;
8659
8780
  }
8660
- log16.info("webhook.send", { event: payload.event, url: targetLabel });
8781
+ log15.info("webhook.send", { event: payload.event, url: targetLabel });
8661
8782
  const maxRetries = 3;
8662
8783
  const delays = [1e3, 4e3, 16e3];
8663
8784
  for (let attempt = 0; attempt < maxRetries; attempt++) {
8664
8785
  try {
8665
- const response = await deliverWebhook(targetCheck.target, payload, webhookSecret);
8786
+ const response = await deliverWebhook(targetCheck.target, body, signingSecret);
8666
8787
  if (response.status >= 200 && response.status < 300) {
8667
- log16.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
8788
+ log15.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
8668
8789
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
8669
8790
  return;
8670
8791
  }
8671
8792
  const errorDetail = response.error ?? `HTTP ${response.status}`;
8672
- log16.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
8793
+ log15.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
8673
8794
  if (attempt === maxRetries - 1) {
8674
8795
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
8675
8796
  }
@@ -8677,7 +8798,7 @@ var Notifier = class {
8677
8798
  const errorDetail = err instanceof Error ? err.message : String(err);
8678
8799
  if (attempt === maxRetries - 1) {
8679
8800
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
8680
- log16.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
8801
+ log15.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
8681
8802
  }
8682
8803
  }
8683
8804
  if (attempt < maxRetries - 1) {
@@ -8687,7 +8808,7 @@ var Notifier = class {
8687
8808
  }
8688
8809
  logDelivery(projectId, notificationId, event, status, error) {
8689
8810
  this.db.insert(auditLog).values({
8690
- id: crypto18.randomUUID(),
8811
+ id: crypto17.randomUUID(),
8691
8812
  projectId,
8692
8813
  actor: "scheduler",
8693
8814
  action: `notification.${status}`,
@@ -8700,8 +8821,8 @@ var Notifier = class {
8700
8821
  };
8701
8822
 
8702
8823
  // src/run-coordinator.ts
8703
- import { eq as eq17 } from "drizzle-orm";
8704
- var log17 = createLogger("RunCoordinator");
8824
+ import { eq as eq16 } from "drizzle-orm";
8825
+ var log16 = createLogger("RunCoordinator");
8705
8826
  var RunCoordinator = class {
8706
8827
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
8707
8828
  this.db = db;
@@ -8716,10 +8837,10 @@ var RunCoordinator = class {
8716
8837
  onInsightsGenerated;
8717
8838
  onAeroEvent;
8718
8839
  async onRunCompleted(runId, projectId) {
8719
- const runRow = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
8840
+ const runRow = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8720
8841
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
8721
8842
  if (runRow?.trigger === RunTriggers.probe) {
8722
- log17.info("probe.skip-side-effects", { runId, projectId, kind });
8843
+ log16.info("probe.skip-side-effects", { runId, projectId, kind });
8723
8844
  return;
8724
8845
  }
8725
8846
  let insightCount = 0;
@@ -8736,12 +8857,12 @@ var RunCoordinator = class {
8736
8857
  try {
8737
8858
  await this.onInsightsGenerated(runId, projectId, result);
8738
8859
  } catch (err) {
8739
- log17.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8860
+ log16.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8740
8861
  }
8741
8862
  }
8742
8863
  }
8743
8864
  } catch (err) {
8744
- log17.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8865
+ log16.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8745
8866
  }
8746
8867
  } else if (kind === RunKinds["gbp-sync"]) {
8747
8868
  try {
@@ -8754,17 +8875,17 @@ var RunCoordinator = class {
8754
8875
  try {
8755
8876
  await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
8756
8877
  } catch (err) {
8757
- log17.error("gbp-insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8878
+ log16.error("gbp-insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8758
8879
  }
8759
8880
  }
8760
8881
  } catch (err) {
8761
- log17.error("gbp-intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8882
+ log16.error("gbp-intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8762
8883
  }
8763
8884
  }
8764
8885
  try {
8765
8886
  await this.notifier.onRunCompleted(runId, projectId);
8766
8887
  } catch (err) {
8767
- log17.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8888
+ log16.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8768
8889
  }
8769
8890
  if (this.onAeroEvent) {
8770
8891
  try {
@@ -8777,7 +8898,7 @@ var RunCoordinator = class {
8777
8898
  };
8778
8899
  await this.onAeroEvent(ctx);
8779
8900
  } catch (err) {
8780
- log17.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8901
+ log16.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8781
8902
  }
8782
8903
  }
8783
8904
  }
@@ -8792,7 +8913,7 @@ var RunCoordinator = class {
8792
8913
  * so the Aero queue is never starved of a follow-up.
8793
8914
  */
8794
8915
  buildDiscoveryAeroContext(runId, projectId, status, error) {
8795
- const session = this.db.select().from(discoverySessions).where(eq17(discoverySessions.runId, runId)).get();
8916
+ const session = this.db.select().from(discoverySessions).where(eq16(discoverySessions.runId, runId)).get();
8796
8917
  const competitorMap = session ? session.competitorMap : [];
8797
8918
  return {
8798
8919
  kind: RunKinds["aeo-discover-probe"],
@@ -8834,8 +8955,8 @@ function analysisResultFromInsights(insights2) {
8834
8955
  }
8835
8956
 
8836
8957
  // src/agent/session-registry.ts
8837
- import crypto22 from "crypto";
8838
- import { eq as eq19 } from "drizzle-orm";
8958
+ import crypto21 from "crypto";
8959
+ import { eq as eq18 } from "drizzle-orm";
8839
8960
 
8840
8961
  // src/agent/session.ts
8841
8962
  import fs7 from "fs";
@@ -9578,7 +9699,7 @@ function buildAeroStateTools(ctx, opts = {}) {
9578
9699
  }
9579
9700
 
9580
9701
  // src/agent/llm-usage.ts
9581
- import crypto19 from "crypto";
9702
+ import crypto18 from "crypto";
9582
9703
  var AeroLlmUsageFeatures = {
9583
9704
  turn: "aero.turn"
9584
9705
  };
@@ -9597,7 +9718,7 @@ function recordLlmUsageEvent(args) {
9597
9718
  const usage = args.message.usage;
9598
9719
  const now = (/* @__PURE__ */ new Date()).toISOString();
9599
9720
  args.db.insert(llmUsageEvents).values({
9600
- id: crypto19.randomUUID(),
9721
+ id: crypto18.randomUUID(),
9601
9722
  projectId: args.projectId,
9602
9723
  runId: args.runId,
9603
9724
  agentSessionId: args.agentSessionId,
@@ -9673,7 +9794,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
9673
9794
  }
9674
9795
 
9675
9796
  // src/agent/tool-usage.ts
9676
- import crypto20 from "crypto";
9797
+ import crypto19 from "crypto";
9677
9798
  var AeroToolEventStatuses = {
9678
9799
  success: "success",
9679
9800
  error: "error"
@@ -9736,7 +9857,7 @@ function createAeroToolUsageHooks(args) {
9736
9857
  function recordAgentToolEvent(args) {
9737
9858
  try {
9738
9859
  args.db.insert(agentToolEvents).values({
9739
- id: crypto20.randomUUID(),
9860
+ id: crypto19.randomUUID(),
9740
9861
  projectId: args.projectId,
9741
9862
  agentSessionId: args.agentSessionId,
9742
9863
  toolCallId: args.toolCallId,
@@ -9879,7 +10000,7 @@ function resolveSessionProviderAndModel(config, opts) {
9879
10000
  import { Type as Type4 } from "@sinclair/typebox";
9880
10001
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9881
10002
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9882
- var log18 = createLogger("RemoteMcp");
10003
+ var log17 = createLogger("RemoteMcp");
9883
10004
  var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
9884
10005
  var MAX_TOOL_RESULT_CHARS2 = 2e4;
9885
10006
  function truncate(text) {
@@ -9932,7 +10053,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
9932
10053
  try {
9933
10054
  client = await connect(server);
9934
10055
  } catch (err) {
9935
- log18.error("external-mcp.connect-failed", {
10056
+ log17.error("external-mcp.connect-failed", {
9936
10057
  label,
9937
10058
  error: err instanceof Error ? err.message : String(err)
9938
10059
  });
@@ -9942,7 +10063,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
9942
10063
  try {
9943
10064
  listed = await client.listTools();
9944
10065
  } catch (err) {
9945
- log18.error("external-mcp.list-failed", {
10066
+ log17.error("external-mcp.list-failed", {
9946
10067
  label,
9947
10068
  error: err instanceof Error ? err.message : String(err)
9948
10069
  });
@@ -9953,7 +10074,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
9953
10074
  if (!isReadOnly(tool)) continue;
9954
10075
  tools.push(adaptRemoteTool(client, tool));
9955
10076
  }
9956
- log18.info("external-mcp.loaded", {
10077
+ log17.info("external-mcp.loaded", {
9957
10078
  label,
9958
10079
  discovered: listed.tools.length,
9959
10080
  adopted: tools.length
@@ -9963,8 +10084,8 @@ async function loadExternalMcpTools(servers, opts = {}) {
9963
10084
  }
9964
10085
 
9965
10086
  // src/agent/memory-store.ts
9966
- import crypto21 from "crypto";
9967
- import { and as and14, desc as desc6, eq as eq18, like, sql as sql6 } from "drizzle-orm";
10087
+ import crypto20 from "crypto";
10088
+ import { and as and13, desc as desc6, eq as eq17, like, sql as sql7 } from "drizzle-orm";
9968
10089
  var COMPACTION_KEY_PREFIX = "compaction:";
9969
10090
  var COMPACTION_NOTES_PER_SESSION = 3;
9970
10091
  function rowToDto(row) {
@@ -9978,7 +10099,7 @@ function rowToDto(row) {
9978
10099
  };
9979
10100
  }
9980
10101
  function listMemoryEntries(db, projectId, opts = {}) {
9981
- const query = db.select().from(agentMemory).where(eq18(agentMemory.projectId, projectId)).orderBy(desc6(agentMemory.updatedAt));
10102
+ const query = db.select().from(agentMemory).where(eq17(agentMemory.projectId, projectId)).orderBy(desc6(agentMemory.updatedAt));
9982
10103
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
9983
10104
  return rows.map(rowToDto);
9984
10105
  }
@@ -9992,7 +10113,7 @@ function upsertMemoryEntry(db, args) {
9992
10113
  throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
9993
10114
  }
9994
10115
  const now = (/* @__PURE__ */ new Date()).toISOString();
9995
- const id = crypto21.randomUUID();
10116
+ const id = crypto20.randomUUID();
9996
10117
  db.insert(agentMemory).values({
9997
10118
  id,
9998
10119
  projectId: args.projectId,
@@ -10009,12 +10130,12 @@ function upsertMemoryEntry(db, args) {
10009
10130
  updatedAt: now
10010
10131
  }
10011
10132
  }).run();
10012
- const row = db.select().from(agentMemory).where(and14(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, args.key))).get();
10133
+ const row = db.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, args.key))).get();
10013
10134
  if (!row) throw new Error("memory upsert produced no row");
10014
10135
  return rowToDto(row);
10015
10136
  }
10016
10137
  function deleteMemoryEntry(db, projectId, key) {
10017
- const result = db.delete(agentMemory).where(and14(eq18(agentMemory.projectId, projectId), eq18(agentMemory.key, key))).run();
10138
+ const result = db.delete(agentMemory).where(and13(eq17(agentMemory.projectId, projectId), eq17(agentMemory.key, key))).run();
10018
10139
  const changes = result.changes ?? 0;
10019
10140
  return changes > 0;
10020
10141
  }
@@ -10029,7 +10150,7 @@ function writeCompactionNote(db, args) {
10029
10150
  }
10030
10151
  const now = (/* @__PURE__ */ new Date()).toISOString();
10031
10152
  const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
10032
- const id = crypto21.randomUUID();
10153
+ const id = crypto20.randomUUID();
10033
10154
  let inserted;
10034
10155
  db.transaction((tx) => {
10035
10156
  tx.insert(agentMemory).values({
@@ -10043,16 +10164,16 @@ function writeCompactionNote(db, args) {
10043
10164
  }).run();
10044
10165
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
10045
10166
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
10046
- and14(
10047
- eq18(agentMemory.projectId, args.projectId),
10167
+ and13(
10168
+ eq17(agentMemory.projectId, args.projectId),
10048
10169
  like(agentMemory.key, `${sessionPrefix}%`)
10049
10170
  )
10050
10171
  ).orderBy(desc6(agentMemory.updatedAt)).all();
10051
10172
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
10052
10173
  if (stale.length > 0) {
10053
- tx.delete(agentMemory).where(sql6`${agentMemory.id} IN (${sql6.join(stale.map((s) => sql6`${s}`), sql6`, `)})`).run();
10174
+ tx.delete(agentMemory).where(sql7`${agentMemory.id} IN (${sql7.join(stale.map((s) => sql7`${s}`), sql7`, `)})`).run();
10054
10175
  }
10055
- const row = tx.select().from(agentMemory).where(and14(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, key))).get();
10176
+ const row = tx.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, key))).get();
10056
10177
  if (row) inserted = rowToDto(row);
10057
10178
  });
10058
10179
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -10185,7 +10306,7 @@ async function compactMessages(args) {
10185
10306
  }
10186
10307
 
10187
10308
  // src/agent/session-registry.ts
10188
- var log19 = createLogger("SessionRegistry");
10309
+ var log18 = createLogger("SessionRegistry");
10189
10310
  var MAX_HYDRATE_NOTES = 20;
10190
10311
  var MAX_HYDRATE_BYTES = 32 * 1024;
10191
10312
  function escapeMemoryFragment(value) {
@@ -10229,7 +10350,7 @@ var SessionRegistry = class {
10229
10350
  if (!this.externalToolsPromise) {
10230
10351
  this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
10231
10352
  (err) => {
10232
- log19.error("external-mcp.load-failed", {
10353
+ log18.error("external-mcp.load-failed", {
10233
10354
  error: err instanceof Error ? err.message : String(err)
10234
10355
  });
10235
10356
  return [];
@@ -10279,7 +10400,7 @@ var SessionRegistry = class {
10279
10400
  modelProvider: effectiveProvider,
10280
10401
  modelId: effectiveModelId,
10281
10402
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10282
- }).where(eq19(agentSessions.projectId, projectId)).run();
10403
+ }).where(eq18(agentSessions.projectId, projectId)).run();
10283
10404
  }
10284
10405
  const agent2 = createAeroSession({
10285
10406
  projectName,
@@ -10308,7 +10429,7 @@ var SessionRegistry = class {
10308
10429
  }
10309
10430
  const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
10310
10431
  const systemPrompt = loadAeroSystemPrompt();
10311
- const sessionId = crypto22.randomUUID();
10432
+ const sessionId = crypto21.randomUUID();
10312
10433
  const agent = createAeroSession({
10313
10434
  projectName,
10314
10435
  client: this.opts.client,
@@ -10474,13 +10595,13 @@ ${lines.join("\n")}
10474
10595
  agent.state.messages = result.messages;
10475
10596
  agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
10476
10597
  this.save(projectName);
10477
- log19.info("compaction.completed", {
10598
+ log18.info("compaction.completed", {
10478
10599
  projectName,
10479
10600
  removedCount: result.removedCount,
10480
10601
  summaryBytes: Buffer.byteLength(result.summary, "utf8")
10481
10602
  });
10482
10603
  } catch (err) {
10483
- log19.error("compaction.failed", {
10604
+ log18.error("compaction.failed", {
10484
10605
  projectName,
10485
10606
  error: err instanceof Error ? err.message : String(err)
10486
10607
  });
@@ -10513,7 +10634,7 @@ ${lines.join("\n")}
10513
10634
  modelProvider: nextProvider,
10514
10635
  modelId: nextModelId,
10515
10636
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10516
- }).where(eq19(agentSessions.projectId, projectId)).run();
10637
+ }).where(eq18(agentSessions.projectId, projectId)).run();
10517
10638
  }
10518
10639
  /** Persist a session's transcript back to the DB. Call after any run settles. */
10519
10640
  save(projectName) {
@@ -10581,7 +10702,7 @@ ${lines.join("\n")}
10581
10702
  await agent.prompt(msgs);
10582
10703
  this.save(projectName);
10583
10704
  } catch (err) {
10584
- log19.error("drain.failed", {
10705
+ log18.error("drain.failed", {
10585
10706
  projectName,
10586
10707
  error: err instanceof Error ? err.message : String(err)
10587
10708
  });
@@ -10677,17 +10798,17 @@ ${lines.join("\n")}
10677
10798
  return id;
10678
10799
  }
10679
10800
  tryResolveProjectId(projectName) {
10680
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq19(projects.name, projectName)).get();
10801
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq18(projects.name, projectName)).get();
10681
10802
  return row?.id;
10682
10803
  }
10683
10804
  loadRow(projectId) {
10684
- const row = this.opts.db.select().from(agentSessions).where(eq19(agentSessions.projectId, projectId)).get();
10805
+ const row = this.opts.db.select().from(agentSessions).where(eq18(agentSessions.projectId, projectId)).get();
10685
10806
  return row ?? null;
10686
10807
  }
10687
10808
  insertRow(params) {
10688
10809
  const now = (/* @__PURE__ */ new Date()).toISOString();
10689
10810
  this.opts.db.insert(agentSessions).values({
10690
- id: params.id ?? crypto22.randomUUID(),
10811
+ id: params.id ?? crypto21.randomUUID(),
10691
10812
  projectId: params.projectId,
10692
10813
  systemPrompt: params.systemPrompt,
10693
10814
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -10700,14 +10821,14 @@ ${lines.join("\n")}
10700
10821
  }
10701
10822
  updateRow(projectId, patch) {
10702
10823
  const now = (/* @__PURE__ */ new Date()).toISOString();
10703
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq19(agentSessions.projectId, projectId)).run();
10824
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq18(agentSessions.projectId, projectId)).run();
10704
10825
  }
10705
10826
  };
10706
10827
 
10707
10828
  // src/agent/agent-routes.ts
10708
- import { eq as eq20 } from "drizzle-orm";
10829
+ import { eq as eq19 } from "drizzle-orm";
10709
10830
  function resolveProject(db, name) {
10710
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq20(projects.name, name)).get();
10831
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq19(projects.name, name)).get();
10711
10832
  if (!row) throw notFound("project", name);
10712
10833
  return row;
10713
10834
  }
@@ -10716,7 +10837,7 @@ function registerAgentRoutes(app, opts) {
10716
10837
  "/projects/:name/agent/transcript",
10717
10838
  async (request) => {
10718
10839
  const project = resolveProject(opts.db, request.params.name);
10719
- const row = opts.db.select().from(agentSessions).where(eq20(agentSessions.projectId, project.id)).get();
10840
+ const row = opts.db.select().from(agentSessions).where(eq19(agentSessions.projectId, project.id)).get();
10720
10841
  if (!row) {
10721
10842
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
10722
10843
  }
@@ -10740,7 +10861,7 @@ function registerAgentRoutes(app, opts) {
10740
10861
  async (request) => {
10741
10862
  const project = resolveProject(opts.db, request.params.name);
10742
10863
  opts.sessionRegistry.reset(project.name);
10743
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(agentSessions.projectId, project.id)).run();
10864
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(agentSessions.projectId, project.id)).run();
10744
10865
  return { status: "reset" };
10745
10866
  }
10746
10867
  );
@@ -11174,7 +11295,7 @@ function formatAuditFactorScore(factor) {
11174
11295
  }
11175
11296
 
11176
11297
  // src/snapshot-service.ts
11177
- var log20 = createLogger("Snapshot");
11298
+ var log19 = createLogger("Snapshot");
11178
11299
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
11179
11300
  var SNAPSHOT_QUERY_COUNT = 6;
11180
11301
  var ProviderExecutionGate2 = class {
@@ -11320,7 +11441,7 @@ var SnapshotService = class {
11320
11441
  return mapAuditReport(report);
11321
11442
  } catch (err) {
11322
11443
  const message = err instanceof Error ? err.message : String(err);
11323
- log20.warn("audit.failed", { homepageUrl, error: message });
11444
+ log19.warn("audit.failed", { homepageUrl, error: message });
11324
11445
  return {
11325
11446
  url: homepageUrl,
11326
11447
  finalUrl: homepageUrl,
@@ -11349,7 +11470,7 @@ var SnapshotService = class {
11349
11470
  queries: parsedQueries
11350
11471
  };
11351
11472
  } catch (err) {
11352
- log20.warn("profile.generation-failed", {
11473
+ log19.warn("profile.generation-failed", {
11353
11474
  domain: ctx.domain,
11354
11475
  provider: ctx.analysisProvider.adapter.name,
11355
11476
  error: err instanceof Error ? err.message : String(err)
@@ -11491,7 +11612,7 @@ var SnapshotService = class {
11491
11612
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
11492
11613
  };
11493
11614
  } catch (err) {
11494
- log20.warn("response.analysis-failed", {
11615
+ log19.warn("response.analysis-failed", {
11495
11616
  provider: ctx.analysisProvider.adapter.name,
11496
11617
  error: err instanceof Error ? err.message : String(err)
11497
11618
  });
@@ -11746,20 +11867,20 @@ function clipText(value, length) {
11746
11867
  }
11747
11868
 
11748
11869
  // src/research-runner.ts
11749
- import { and as and15, eq as eq21, inArray as inArray7, sql as sql7 } from "drizzle-orm";
11870
+ import { and as and14, eq as eq20, inArray as inArray7, sql as sql8 } from "drizzle-orm";
11750
11871
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
11751
11872
  function finalResearchRunStatus(completed, failed) {
11752
11873
  if (failed === 0) return ResearchRunStatuses.completed;
11753
11874
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
11754
11875
  }
11755
11876
  async function executeResearchRun(db, registry, runId, projectId) {
11756
- const run = db.select().from(researchRuns).where(and15(eq21(researchRuns.id, runId), eq21(researchRuns.projectId, projectId))).get();
11877
+ const run = db.select().from(researchRuns).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.projectId, projectId))).get();
11757
11878
  if (!run || run.status !== ResearchRunStatuses.queued) return;
11758
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
11879
+ const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
11759
11880
  if (!project) return;
11760
11881
  const provider = registry.get(run.provider);
11761
11882
  const now = (/* @__PURE__ */ new Date()).toISOString();
11762
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and15(eq21(researchRuns.id, runId), eq21(researchRuns.status, ResearchRunStatuses.queued))).run();
11883
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.status, ResearchRunStatuses.queued))).run();
11763
11884
  if (claim.changes !== 1) return;
11764
11885
  let reserved = 0;
11765
11886
  let dispatched = 0;
@@ -11783,8 +11904,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
11783
11904
  }
11784
11905
  reserved = run.totalQueries;
11785
11906
  reservation = { scope, period };
11786
- const rows = db.select().from(researchRunQueries).where(eq21(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
11787
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq21(competitors.projectId, projectId)).all().map((row) => row.domain);
11907
+ const rows = db.select().from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
11908
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq20(competitors.projectId, projectId)).all().map((row) => row.domain);
11788
11909
  const domains = effectiveDomains(project);
11789
11910
  const brands = effectiveBrandNames(project);
11790
11911
  const config = { ...provider.config, model: run.resolvedModel };
@@ -11792,7 +11913,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
11792
11913
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
11793
11914
  try {
11794
11915
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11795
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and15(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
11916
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
11796
11917
  const raw = await gate.run(async () => {
11797
11918
  dispatched++;
11798
11919
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -11819,7 +11940,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
11819
11940
  citationState: determineCitationState(normalized, domains),
11820
11941
  rawResponse: raw.rawResponse,
11821
11942
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
11822
- }).where(and15(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.running))).run();
11943
+ }).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.running))).run();
11823
11944
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
11824
11945
  } catch (error) {
11825
11946
  try {
@@ -11843,18 +11964,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
11843
11964
  }
11844
11965
  }
11845
11966
  function incrementResearchProgress(db, runId, column) {
11846
- db.update(researchRuns).set({ [column]: sql7`${researchRuns[column]} + 1` }).where(eq21(researchRuns.id, runId)).run();
11967
+ db.update(researchRuns).set({ [column]: sql8`${researchRuns[column]} + 1` }).where(eq20(researchRuns.id, runId)).run();
11847
11968
  }
11848
11969
  function markResearchQueryFailed(db, runId, queryId, error) {
11849
11970
  const message = error instanceof Error ? error.message : String(error);
11850
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and15(eq21(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11971
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq20(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11851
11972
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
11852
11973
  }
11853
11974
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
11854
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and15(eq21(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11975
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq20(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11855
11976
  }
11856
11977
  function finalizeResearchRun(db, runId, fatalError) {
11857
- const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq21(researchRunQueries.researchRunId, runId)).all();
11978
+ const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).all();
11858
11979
  const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
11859
11980
  const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
11860
11981
  db.update(researchRuns).set({
@@ -11863,13 +11984,13 @@ function finalizeResearchRun(db, runId, fatalError) {
11863
11984
  failedQueries: failed,
11864
11985
  error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
11865
11986
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
11866
- }).where(eq21(researchRuns.id, runId)).run();
11987
+ }).where(eq20(researchRuns.id, runId)).run();
11867
11988
  }
11868
11989
 
11869
11990
  // src/server.ts
11870
11991
  var _require3 = createRequire3(import.meta.url);
11871
11992
  var { version: PKG_VERSION2 } = _require3("../package.json");
11872
- var log21 = createLogger("Server");
11993
+ var log20 = createLogger("Server");
11873
11994
  var DEFAULT_QUOTA = {
11874
11995
  maxConcurrency: 2,
11875
11996
  maxRequestsPerMinute: 10,
@@ -11887,6 +12008,9 @@ function parseBooleanEnv(value) {
11887
12008
  function resolveDashboardRequirePassword(env, config) {
11888
12009
  return parseBooleanEnv(env.CANONRY_DASHBOARD_REQUIRE_PASSWORD) ?? config.dashboard?.requirePassword ?? true;
11889
12010
  }
12011
+ function resolveDashboardShowResourceLinks(env, config) {
12012
+ return parseBooleanEnv(env.CANONRY_DASHBOARD_SHOW_RESOURCE_LINKS) ?? config.dashboard?.showResourceLinks ?? true;
12013
+ }
11890
12014
  var API_ADAPTERS = [
11891
12015
  geminiAdapter,
11892
12016
  openaiAdapter,
@@ -11912,14 +12036,14 @@ function summarizeProviderConfig(config) {
11912
12036
  };
11913
12037
  }
11914
12038
  function hashApiKey(key) {
11915
- return crypto23.createHash("sha256").update(key).digest("hex");
12039
+ return crypto22.createHash("sha256").update(key).digest("hex");
11916
12040
  }
11917
12041
  var DASHBOARD_SCRYPT_KEYLEN = 64;
11918
12042
  var DASHBOARD_SCRYPT_COST = 1 << 15;
11919
12043
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
11920
12044
  function hashDashboardPassword(password) {
11921
- const salt = crypto23.randomBytes(16);
11922
- const derived = crypto23.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
12045
+ const salt = crypto22.randomBytes(16);
12046
+ const derived = crypto22.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
11923
12047
  N: DASHBOARD_SCRYPT_COST,
11924
12048
  maxmem: DASHBOARD_SCRYPT_MAXMEM
11925
12049
  });
@@ -11940,14 +12064,14 @@ function verifyDashboardPassword(password, storedHash) {
11940
12064
  } catch {
11941
12065
  return { ok: false, needsRehash: false };
11942
12066
  }
11943
- const derived = crypto23.scryptSync(password, salt, expected.length, {
12067
+ const derived = crypto22.scryptSync(password, salt, expected.length, {
11944
12068
  N: DASHBOARD_SCRYPT_COST,
11945
12069
  maxmem: DASHBOARD_SCRYPT_MAXMEM
11946
12070
  });
11947
12071
  if (derived.length !== expected.length)
11948
12072
  return { ok: false, needsRehash: false };
11949
12073
  return {
11950
- ok: crypto23.timingSafeEqual(derived, expected),
12074
+ ok: crypto22.timingSafeEqual(derived, expected),
11951
12075
  needsRehash: false
11952
12076
  };
11953
12077
  }
@@ -11956,7 +12080,7 @@ function verifyDashboardPassword(password, storedHash) {
11956
12080
  const expected = Buffer.from(storedHash, "hex");
11957
12081
  if (candidate.length !== expected.length)
11958
12082
  return { ok: false, needsRehash: false };
11959
- const ok = crypto23.timingSafeEqual(candidate, expected);
12083
+ const ok = crypto22.timingSafeEqual(candidate, expected);
11960
12084
  return { ok, needsRehash: ok };
11961
12085
  }
11962
12086
  return { ok: false, needsRehash: false };
@@ -12019,7 +12143,7 @@ function applyLegacyCredentials(rows, config) {
12019
12143
  }
12020
12144
  if (migratedGoogle > 0) {
12021
12145
  saveConfigPatch({ google: config.google });
12022
- log21.info("credentials.migrated", { type: "google", count: migratedGoogle });
12146
+ log20.info("credentials.migrated", { type: "google", count: migratedGoogle });
12023
12147
  }
12024
12148
  let migratedGa4 = 0;
12025
12149
  for (const row of rows.ga4) {
@@ -12037,7 +12161,7 @@ function applyLegacyCredentials(rows, config) {
12037
12161
  }
12038
12162
  if (migratedGa4 > 0) {
12039
12163
  saveConfigPatch({ ga4: config.ga4 });
12040
- log21.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
12164
+ log20.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
12041
12165
  }
12042
12166
  }
12043
12167
  function isLoopbackBindHost(host) {
@@ -12094,11 +12218,11 @@ async function createServer(opts) {
12094
12218
  applyLegacyCredentials(legacyRows, opts.config);
12095
12219
  dropLegacyCredentialColumns(opts.db);
12096
12220
  } catch (err) {
12097
- log21.warn("credentials.migration.failed", {
12221
+ log20.warn("credentials.migration.failed", {
12098
12222
  error: err instanceof Error ? err.message : String(err)
12099
12223
  });
12100
12224
  }
12101
- log21.info("providers.configured", {
12225
+ log20.info("providers.configured", {
12102
12226
  providers: Object.keys(providers).filter((k) => {
12103
12227
  const p = providers[k];
12104
12228
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
@@ -12162,7 +12286,7 @@ async function createServer(opts) {
12162
12286
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
12163
12287
  async (ctx) => {
12164
12288
  if (!sessionRegistry) return;
12165
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq22(projects.id, ctx.projectId)).get();
12289
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq21(projects.id, ctx.projectId)).get();
12166
12290
  if (!project) return;
12167
12291
  let content;
12168
12292
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -12497,6 +12621,24 @@ async function createServer(opts) {
12497
12621
  onAdsSyncRequested: (runId, projectId) => {
12498
12622
  runAdsSync(runId, projectId);
12499
12623
  },
12624
+ onDoctorRequested: (projectName) => {
12625
+ void (async () => {
12626
+ try {
12627
+ const report = await aeroClient.runDoctor({ project: projectName });
12628
+ const project = opts.db.select().from(projects).where(eq21(projects.name, projectName)).get();
12629
+ if (!project) {
12630
+ app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
12631
+ return;
12632
+ }
12633
+ await notifier.onHealthChecked(project.id, {
12634
+ checks: report.checks,
12635
+ checkedAt: report.generatedAt
12636
+ });
12637
+ } catch (err) {
12638
+ app.log.warn({ projectName, err }, "scheduled doctor pass failed");
12639
+ }
12640
+ })();
12641
+ },
12500
12642
  onDataRefreshRequested: (projectName) => {
12501
12643
  void refreshAllIntegrations(aeroClient, projectName);
12502
12644
  },
@@ -12511,9 +12653,9 @@ async function createServer(opts) {
12511
12653
  });
12512
12654
  if (!probed) return;
12513
12655
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
12514
- and16(
12515
- eq22(ccReleaseSyncs.release, probed.release),
12516
- eq22(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
12656
+ and15(
12657
+ eq21(ccReleaseSyncs.release, probed.release),
12658
+ eq21(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
12517
12659
  )
12518
12660
  ).limit(1).get();
12519
12661
  if (alreadySynced) {
@@ -12534,18 +12676,6 @@ async function createServer(opts) {
12534
12676
  );
12535
12677
  });
12536
12678
  })();
12537
- const project = opts.db.select().from(projects).where(eq22(projects.name, projectName)).get();
12538
- if (project && bingConnectionStore.getConnection(project.canonicalDomain)) {
12539
- aeroClient.backlinksBingSync(projectName).catch((err) => {
12540
- app.log.error(
12541
- {
12542
- projectName,
12543
- err: err instanceof Error ? err.message : String(err)
12544
- },
12545
- "Scheduled Bing backlinks sync failed"
12546
- );
12547
- });
12548
- }
12549
12679
  },
12550
12680
  onSiteAuditRequested: (runId, projectId) => {
12551
12681
  runSiteAudit(runId, projectId);
@@ -12680,7 +12810,7 @@ async function createServer(opts) {
12680
12810
  return removed;
12681
12811
  }
12682
12812
  };
12683
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
12813
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
12684
12814
  const googleConnectionStore = {
12685
12815
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
12686
12816
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -12745,6 +12875,7 @@ async function createServer(opts) {
12745
12875
  );
12746
12876
  }
12747
12877
  const dashboardRequirePassword = resolveDashboardRequirePassword(process.env, opts.config);
12878
+ const dashboardShowResourceLinks = resolveDashboardShowResourceLinks(process.env, opts.config);
12748
12879
  app.log.info(
12749
12880
  { dashboardRequirePassword },
12750
12881
  "Dashboard password gate resolved"
@@ -12753,11 +12884,11 @@ async function createServer(opts) {
12753
12884
  const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
12754
12885
  if (opts.config.apiKey) {
12755
12886
  const keyHash = hashApiKey(opts.config.apiKey);
12756
- const existing = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, keyHash)).get();
12887
+ const existing = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, keyHash)).get();
12757
12888
  if (!existing) {
12758
12889
  const prefix = opts.config.apiKey.slice(0, 12);
12759
12890
  opts.db.insert(apiKeys).values({
12760
- id: `key_${crypto23.randomBytes(8).toString("hex")}`,
12891
+ id: `key_${crypto22.randomBytes(8).toString("hex")}`,
12761
12892
  name: "default",
12762
12893
  keyHash,
12763
12894
  keyPrefix: prefix,
@@ -12781,7 +12912,7 @@ async function createServer(opts) {
12781
12912
  };
12782
12913
  const createSession = (apiKeyId) => {
12783
12914
  pruneExpiredSessions();
12784
- const sessionId = crypto23.randomBytes(32).toString("hex");
12915
+ const sessionId = crypto22.randomBytes(32).toString("hex");
12785
12916
  sessions.set(sessionId, {
12786
12917
  apiKeyId,
12787
12918
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -12805,7 +12936,7 @@ async function createServer(opts) {
12805
12936
  };
12806
12937
  const getDefaultApiKey = () => {
12807
12938
  if (!opts.config.apiKey) return void 0;
12808
- return opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
12939
+ return opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
12809
12940
  };
12810
12941
  const createPasswordSession = (reply) => {
12811
12942
  const key = getDefaultApiKey();
@@ -12829,7 +12960,7 @@ async function createServer(opts) {
12829
12960
  if (!header) return false;
12830
12961
  const parts = header.split(" ");
12831
12962
  if (parts.length !== 2 || parts[0] !== "Bearer") return false;
12832
- const key = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(parts[1]))).get();
12963
+ const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(parts[1]))).get();
12833
12964
  return Boolean(key && !key.revokedAt);
12834
12965
  };
12835
12966
  app.get(apiPrefix + "/session", async (request, reply) => {
@@ -12906,12 +13037,12 @@ async function createServer(opts) {
12906
13037
  return reply.send({ authenticated: true });
12907
13038
  }
12908
13039
  if (apiKey) {
12909
- const key = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(apiKey))).get();
13040
+ const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(apiKey))).get();
12910
13041
  if (!key || key.revokedAt) {
12911
13042
  const err2 = authInvalid();
12912
13043
  return reply.status(err2.statusCode).send(err2.toJSON());
12913
13044
  }
12914
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, key.id)).run();
13045
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(apiKeys.id, key.id)).run();
12915
13046
  const sessionId = createSession(key.id);
12916
13047
  reply.header(
12917
13048
  "set-cookie",
@@ -13022,6 +13153,11 @@ async function createServer(opts) {
13022
13153
  // installer; cloud deployments inherit the secure default of `false` by
13023
13154
  // not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
13024
13155
  allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
13156
+ // Wall-clock budget for one incremental Vercel drain. This is the only lever
13157
+ // that decides whether a source catches up or falls further behind, and it
13158
+ // was previously reachable only from tests, so a source losing ground could
13159
+ // not be rescued without shipping code. Unset keeps the built-in default.
13160
+ vercelSyncDeadlineMs: resolveVercelSyncDeadlineMs(process.env),
13025
13161
  // Local-only Aero agent routes. Registered here so they inherit api-routes'
13026
13162
  // auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
13027
13163
  registerAuthenticatedRoutes: async (scope) => {
@@ -13104,7 +13240,7 @@ async function createServer(opts) {
13104
13240
  deps: {
13105
13241
  enqueueAutoExtract: ({ projectId, release: r }) => {
13106
13242
  const now = (/* @__PURE__ */ new Date()).toISOString();
13107
- const runId = crypto23.randomUUID();
13243
+ const runId = crypto22.randomUUID();
13108
13244
  opts.db.insert(runs).values({
13109
13245
  id: runId,
13110
13246
  projectId,
@@ -13134,13 +13270,6 @@ async function createServer(opts) {
13134
13270
  }
13135
13271
  );
13136
13272
  },
13137
- onBingBacklinkSyncRequested: (runId, projectId) => {
13138
- executeBingBacklinkSync(opts.db, runId, projectId, {
13139
- resolveConnection: (domain) => bingConnectionStore.getConnection(domain)
13140
- }).catch((err) => {
13141
- app.log.error({ runId, err }, "Bing backlink sync failed");
13142
- });
13143
- },
13144
13273
  onDiscoveryRunRequested: (input) => {
13145
13274
  executeDiscoveryRun({
13146
13275
  db: opts.db,
@@ -13236,7 +13365,7 @@ async function createServer(opts) {
13236
13365
  ...inspectOpts,
13237
13366
  config: opts.config
13238
13367
  }).then(() => {
13239
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq22(runs.id, runId)).get();
13368
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq21(runs.id, runId)).get();
13240
13369
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
13241
13370
  return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
13242
13371
  }
@@ -13330,7 +13459,7 @@ async function createServer(opts) {
13330
13459
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
13331
13460
  opts.db.insert(auditLog).values(
13332
13461
  targetProjectIds.map((projectId) => ({
13333
- id: crypto23.randomUUID(),
13462
+ id: crypto22.randomUUID(),
13334
13463
  projectId,
13335
13464
  actor: "api",
13336
13465
  action: existing ? "provider.updated" : "provider.created",
@@ -13496,6 +13625,9 @@ async function createServer(opts) {
13496
13625
  const injectConfig = (html, projectTabsOverride, themeOverride, renderTokenOverride) => {
13497
13626
  const clientConfig = {};
13498
13627
  if (basePath) clientConfig.basePath = basePath;
13628
+ if (!dashboardShowResourceLinks) {
13629
+ clientConfig.dashboard = { showResourceLinks: false };
13630
+ }
13499
13631
  if (embed.enabled) {
13500
13632
  const embedClient = embedClientConfigForRequest(embed, projectTabsOverride, themeOverride, renderTokenOverride);
13501
13633
  if (embedClient) clientConfig.embed = embedClient;
@@ -13585,7 +13717,7 @@ async function createServer(opts) {
13585
13717
  }
13586
13718
  checkLatestVersionForServer();
13587
13719
  scheduler.start();
13588
- for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq22(researchRuns.status, ResearchRunStatuses.queued)).all()) {
13720
+ for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq21(researchRuns.status, ResearchRunStatuses.queued)).all()) {
13589
13721
  dispatchResearchRun(run.id, run.projectId);
13590
13722
  }
13591
13723
  app.addHook("onClose", async () => {