@canonry/canonry 4.138.0 → 4.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -13
- package/assets/assets/{AuditHistoryPanel-CIdoSKjP.js → AuditHistoryPanel-DL9chF3F.js} +1 -1
- package/assets/assets/BacklinksPage-sA_tgQJv.js +1 -0
- package/assets/assets/ChartPrimitives-D7nLxIbe.js +1 -0
- package/assets/assets/HistoryPage-CLWbcu5m.js +1 -0
- package/assets/assets/ProjectPage-C_oXMCNU.js +8 -0
- package/assets/assets/{RunRow-D6W6D1BX.js → RunRow-DQ4icEiv.js} +1 -1
- package/assets/assets/RunsPage-D5sizm9T.js +1 -0
- package/assets/assets/SettingsPage-XdQjvWsg.js +1 -0
- package/assets/assets/TrafficPage-C12osXDa.js +1 -0
- package/assets/assets/TrafficSourceDetailPage-CXAiNK8Y.js +1 -0
- package/assets/assets/{arrow-left-ClC65OiR.js → arrow-left-LaI5a16g.js} +1 -1
- package/assets/assets/extract-error-message-CYWaTM3m.js +1 -0
- package/assets/assets/index-CguwySA5.js +213 -0
- package/assets/assets/index-CntuM6Th.css +1 -0
- package/assets/assets/trash-2-DoH00q7m.js +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-3KGJ7BXW.js → chunk-DLKUDNDA.js} +531 -435
- package/dist/{chunk-GO24URVP.js → chunk-IGU67QFM.js} +1607 -1149
- package/dist/{chunk-R3GAWKHM.js → chunk-IYTHIAAE.js} +5 -25
- package/dist/{chunk-RJ7OW6W6.js → chunk-OA2EGCUS.js} +2973 -2914
- package/dist/cli.js +10 -38
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-5RJHJLYA.js → intelligence-service-SMYVN3KN.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
- package/assets/assets/BacklinksPage-BYnzQLFq.js +0 -1
- package/assets/assets/ChartPrimitives-bx4p5MDx.js +0 -1
- package/assets/assets/HistoryPage-CjLb8TKY.js +0 -1
- package/assets/assets/ProjectPage-C_bSGiLs.js +0 -8
- package/assets/assets/RunsPage-CQT4lXFp.js +0 -1
- package/assets/assets/SettingsPage-DVQKzz6s.js +0 -1
- package/assets/assets/TrafficPage-BdocLgWw.js +0 -1
- package/assets/assets/TrafficSourceDetailPage-CTZF_R9C.js +0 -1
- package/assets/assets/extract-error-message-CsgTjzyj.js +0 -1
- package/assets/assets/index-BZgknVrZ.js +0 -213
- package/assets/assets/index-DY7GziwY.css +0 -1
- 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-
|
|
13
|
+
} from "./chunk-IYTHIAAE.js";
|
|
14
14
|
import {
|
|
15
15
|
CC_CACHE_DIR,
|
|
16
16
|
DUCKDB_SPEC,
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
deliverWebhook,
|
|
51
51
|
determineCitationState,
|
|
52
52
|
discoverySessions,
|
|
53
|
+
doctorHealthState,
|
|
53
54
|
downloadFile,
|
|
54
55
|
dropLegacyCredentialColumns,
|
|
55
56
|
executeDiscovery,
|
|
@@ -70,11 +71,9 @@ import {
|
|
|
70
71
|
gbpPlaceDetails,
|
|
71
72
|
getAttributes,
|
|
72
73
|
getCrawlIssues,
|
|
73
|
-
getLinkCounts,
|
|
74
74
|
getLodging,
|
|
75
75
|
getPlaceDetails,
|
|
76
76
|
getUrlInfo,
|
|
77
|
-
getUrlLinks,
|
|
78
77
|
groupRunsByCreatedAt,
|
|
79
78
|
gscCoverageSnapshots,
|
|
80
79
|
gscDailyTotals,
|
|
@@ -113,13 +112,16 @@ import {
|
|
|
113
112
|
refreshAccessToken,
|
|
114
113
|
researchRunQueries,
|
|
115
114
|
researchRuns,
|
|
115
|
+
resolveDestination,
|
|
116
|
+
resolveVercelSyncDeadlineMs,
|
|
116
117
|
resolveWebhookTarget,
|
|
117
118
|
runs,
|
|
118
119
|
schedules,
|
|
119
120
|
siteAuditPages,
|
|
120
121
|
siteAuditSnapshots,
|
|
122
|
+
toAlertView,
|
|
121
123
|
usageCounters
|
|
122
|
-
} from "./chunk-
|
|
124
|
+
} from "./chunk-IGU67QFM.js";
|
|
123
125
|
import {
|
|
124
126
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
125
127
|
AGENT_PROVIDER_IDS,
|
|
@@ -206,7 +208,7 @@ import {
|
|
|
206
208
|
validationError,
|
|
207
209
|
winnabilityClassLabel,
|
|
208
210
|
withRetry
|
|
209
|
-
} from "./chunk-
|
|
211
|
+
} from "./chunk-OA2EGCUS.js";
|
|
210
212
|
|
|
211
213
|
// src/telemetry.ts
|
|
212
214
|
import crypto from "crypto";
|
|
@@ -537,11 +539,11 @@ function checkLatestVersionForServer(opts) {
|
|
|
537
539
|
|
|
538
540
|
// src/server.ts
|
|
539
541
|
import { createRequire as createRequire3 } from "module";
|
|
540
|
-
import
|
|
542
|
+
import crypto22 from "crypto";
|
|
541
543
|
import fs8 from "fs";
|
|
542
544
|
import path9 from "path";
|
|
543
545
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
544
|
-
import { and as
|
|
546
|
+
import { and as and15, eq as eq21 } from "drizzle-orm";
|
|
545
547
|
import Fastify from "fastify";
|
|
546
548
|
import os5 from "os";
|
|
547
549
|
|
|
@@ -958,7 +960,14 @@ var geminiAdapter = {
|
|
|
958
960
|
model: raw.model,
|
|
959
961
|
servedModel: raw.servedModel,
|
|
960
962
|
groundingSources: raw.groundingSources,
|
|
961
|
-
searchQueries: raw.searchQueries
|
|
963
|
+
searchQueries: raw.searchQueries,
|
|
964
|
+
// Retrieval detection is not implemented for this provider. Its candidate
|
|
965
|
+
// marker is present on 100% of stored rows, so it has never been shown to
|
|
966
|
+
// discriminate a non-retrieving answer and wiring it up would hardcode
|
|
967
|
+
// `used`. `unknown` states what we actually know. The contract is a
|
|
968
|
+
// declaration about how we build the request, so it is always knowable.
|
|
969
|
+
retrievalStatus: "unknown",
|
|
970
|
+
retrievalContract: "native-auto-v1"
|
|
962
971
|
};
|
|
963
972
|
},
|
|
964
973
|
normalizeResult(raw) {
|
|
@@ -975,7 +984,8 @@ var geminiAdapter = {
|
|
|
975
984
|
answerText: normalized.answerText,
|
|
976
985
|
citedDomains: normalized.citedDomains,
|
|
977
986
|
groundingSources: normalized.groundingSources,
|
|
978
|
-
searchQueries: normalized.searchQueries
|
|
987
|
+
searchQueries: normalized.searchQueries,
|
|
988
|
+
retrievalStatus: "unknown"
|
|
979
989
|
};
|
|
980
990
|
},
|
|
981
991
|
async generateText(prompt, config) {
|
|
@@ -1300,7 +1310,14 @@ var openaiAdapter = {
|
|
|
1300
1310
|
model: raw.model,
|
|
1301
1311
|
servedModel: raw.servedModel,
|
|
1302
1312
|
groundingSources: raw.groundingSources,
|
|
1303
|
-
searchQueries: raw.searchQueries
|
|
1313
|
+
searchQueries: raw.searchQueries,
|
|
1314
|
+
// Retrieval detection is not implemented for this provider. Its candidate
|
|
1315
|
+
// marker is present on 100% of stored rows, so it has never been shown to
|
|
1316
|
+
// discriminate a non-retrieving answer and wiring it up would hardcode
|
|
1317
|
+
// `used`. `unknown` states what we actually know. The contract is a
|
|
1318
|
+
// declaration about how we build the request, so it is always knowable.
|
|
1319
|
+
retrievalStatus: "unknown",
|
|
1320
|
+
retrievalContract: "native-auto-v1"
|
|
1304
1321
|
};
|
|
1305
1322
|
},
|
|
1306
1323
|
normalizeResult(raw) {
|
|
@@ -1317,7 +1334,8 @@ var openaiAdapter = {
|
|
|
1317
1334
|
answerText: normalized.answerText,
|
|
1318
1335
|
citedDomains: normalized.citedDomains,
|
|
1319
1336
|
groundingSources: normalized.groundingSources,
|
|
1320
|
-
searchQueries: normalized.searchQueries
|
|
1337
|
+
searchQueries: normalized.searchQueries,
|
|
1338
|
+
retrievalStatus: "unknown"
|
|
1321
1339
|
};
|
|
1322
1340
|
},
|
|
1323
1341
|
async generateText(prompt, config) {
|
|
@@ -1350,6 +1368,7 @@ async function withRetry4(fn, options = {}) {
|
|
|
1350
1368
|
// ../provider-claude/src/normalize.ts
|
|
1351
1369
|
var DEFAULT_MODEL3 = "claude-sonnet-4-6";
|
|
1352
1370
|
var VALIDATION_PATTERN = /^claude-/;
|
|
1371
|
+
var CLAUDE_RETRIEVAL_CONTRACT = "search-required-v1";
|
|
1353
1372
|
function resolveModel2(config) {
|
|
1354
1373
|
const m = config.model;
|
|
1355
1374
|
if (!m) return DEFAULT_MODEL3;
|
|
@@ -1424,6 +1443,9 @@ async function executeTrackedQuery3(input) {
|
|
|
1424
1443
|
model,
|
|
1425
1444
|
max_tokens: 4096,
|
|
1426
1445
|
tools: [webSearchTool],
|
|
1446
|
+
// search-required-v1: retrieval is guaranteed by the API control, so the
|
|
1447
|
+
// query text and answer substance stay untouched. See the contract note.
|
|
1448
|
+
tool_choice: { type: "tool", name: "web_search" },
|
|
1427
1449
|
messages: [{ role: "user", content: input.query }]
|
|
1428
1450
|
})
|
|
1429
1451
|
);
|
|
@@ -1438,7 +1460,9 @@ async function executeTrackedQuery3(input) {
|
|
|
1438
1460
|
model,
|
|
1439
1461
|
servedModel: extractServedModel3(rawResponse),
|
|
1440
1462
|
groundingSources: parsed.groundingSources,
|
|
1441
|
-
searchQueries: parsed.searchQueries
|
|
1463
|
+
searchQueries: parsed.searchQueries,
|
|
1464
|
+
retrievalStatus: parsed.retrievalStatus,
|
|
1465
|
+
retrievalContract: CLAUDE_RETRIEVAL_CONTRACT
|
|
1442
1466
|
};
|
|
1443
1467
|
} catch (err) {
|
|
1444
1468
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1456,7 +1480,8 @@ function normalizeResult3(raw) {
|
|
|
1456
1480
|
answerText: parsed.answerText,
|
|
1457
1481
|
citedDomains,
|
|
1458
1482
|
groundingSources,
|
|
1459
|
-
searchQueries
|
|
1483
|
+
searchQueries,
|
|
1484
|
+
retrievalStatus: useParsed ? parsed.retrievalStatus : raw.retrievalStatus
|
|
1460
1485
|
};
|
|
1461
1486
|
}
|
|
1462
1487
|
function hasParsedResponseContent3(rawResponse) {
|
|
@@ -1475,6 +1500,7 @@ function reparseStoredResult3(rawResponse) {
|
|
|
1475
1500
|
citedDomains: extractCitedDomainsFromSources3(groundingSources),
|
|
1476
1501
|
groundingSources,
|
|
1477
1502
|
searchQueries,
|
|
1503
|
+
retrievalStatus: extractRetrievalStatusFromRaw(rawResponse),
|
|
1478
1504
|
...providerErrors.length > 0 ? { providerError: `web_search tool error: ${providerErrors.join(", ")}` } : {}
|
|
1479
1505
|
};
|
|
1480
1506
|
}
|
|
@@ -1514,6 +1540,17 @@ function extractGroundingSourcesFromRaw2(rawResponse) {
|
|
|
1514
1540
|
}
|
|
1515
1541
|
return sources;
|
|
1516
1542
|
}
|
|
1543
|
+
function extractRetrievalStatusFromRaw(rawResponse) {
|
|
1544
|
+
try {
|
|
1545
|
+
const content = rawResponse.content;
|
|
1546
|
+
if (!Array.isArray(content) || content.length === 0) return "unknown";
|
|
1547
|
+
return content.some(
|
|
1548
|
+
(block) => block.type === "server_tool_use" && block.name === "web_search"
|
|
1549
|
+
) ? "used" : "not-used";
|
|
1550
|
+
} catch {
|
|
1551
|
+
return "unknown";
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1517
1554
|
function extractSearchQueriesFromRaw3(rawResponse) {
|
|
1518
1555
|
const queries2 = /* @__PURE__ */ new Set();
|
|
1519
1556
|
try {
|
|
@@ -1661,7 +1698,9 @@ var claudeAdapter = {
|
|
|
1661
1698
|
model: raw.model,
|
|
1662
1699
|
servedModel: raw.servedModel,
|
|
1663
1700
|
groundingSources: raw.groundingSources,
|
|
1664
|
-
searchQueries: raw.searchQueries
|
|
1701
|
+
searchQueries: raw.searchQueries,
|
|
1702
|
+
retrievalStatus: raw.retrievalStatus,
|
|
1703
|
+
retrievalContract: raw.retrievalContract
|
|
1665
1704
|
};
|
|
1666
1705
|
},
|
|
1667
1706
|
normalizeResult(raw) {
|
|
@@ -1670,7 +1709,12 @@ var claudeAdapter = {
|
|
|
1670
1709
|
rawResponse: raw.rawResponse,
|
|
1671
1710
|
model: raw.model,
|
|
1672
1711
|
groundingSources: raw.groundingSources,
|
|
1673
|
-
searchQueries: raw.searchQueries
|
|
1712
|
+
searchQueries: raw.searchQueries,
|
|
1713
|
+
// The shared RawQueryResult now carries retrieval, so nothing is lost
|
|
1714
|
+
// across this boundary. A reconstruction that predates the field falls
|
|
1715
|
+
// back to `unknown` rather than asserting an absence.
|
|
1716
|
+
retrievalStatus: raw.retrievalStatus ?? "unknown",
|
|
1717
|
+
retrievalContract: raw.retrievalContract ?? CLAUDE_RETRIEVAL_CONTRACT
|
|
1674
1718
|
};
|
|
1675
1719
|
const normalized = normalizeResult3(claudeRaw);
|
|
1676
1720
|
return {
|
|
@@ -1678,7 +1722,8 @@ var claudeAdapter = {
|
|
|
1678
1722
|
answerText: normalized.answerText,
|
|
1679
1723
|
citedDomains: normalized.citedDomains,
|
|
1680
1724
|
groundingSources: normalized.groundingSources,
|
|
1681
|
-
searchQueries: normalized.searchQueries
|
|
1725
|
+
searchQueries: normalized.searchQueries,
|
|
1726
|
+
retrievalStatus: normalized.retrievalStatus
|
|
1682
1727
|
};
|
|
1683
1728
|
},
|
|
1684
1729
|
async generateText(prompt, config) {
|
|
@@ -1894,7 +1939,11 @@ var localAdapter = {
|
|
|
1894
1939
|
model: raw.model,
|
|
1895
1940
|
servedModel: raw.servedModel,
|
|
1896
1941
|
groundingSources: raw.groundingSources,
|
|
1897
|
-
searchQueries: raw.searchQueries
|
|
1942
|
+
searchQueries: raw.searchQueries,
|
|
1943
|
+
// A plain chat completion with no web access: there is no retrieval step
|
|
1944
|
+
// to report, which is what `not-applicable` means.
|
|
1945
|
+
retrievalStatus: "not-applicable",
|
|
1946
|
+
retrievalContract: "native-auto-v1"
|
|
1898
1947
|
};
|
|
1899
1948
|
},
|
|
1900
1949
|
normalizeResult(raw) {
|
|
@@ -1911,7 +1960,8 @@ var localAdapter = {
|
|
|
1911
1960
|
answerText: normalized.answerText,
|
|
1912
1961
|
citedDomains: normalized.citedDomains,
|
|
1913
1962
|
groundingSources: normalized.groundingSources,
|
|
1914
|
-
searchQueries: normalized.searchQueries
|
|
1963
|
+
searchQueries: normalized.searchQueries,
|
|
1964
|
+
retrievalStatus: "not-applicable"
|
|
1915
1965
|
};
|
|
1916
1966
|
},
|
|
1917
1967
|
async generateText(prompt, config) {
|
|
@@ -2360,7 +2410,8 @@ function normalizeResult5(raw) {
|
|
|
2360
2410
|
answerText,
|
|
2361
2411
|
citedDomains: extractCitedDomains(raw.groundingSources),
|
|
2362
2412
|
groundingSources: raw.groundingSources,
|
|
2363
|
-
searchQueries: raw.searchQueries
|
|
2413
|
+
searchQueries: raw.searchQueries,
|
|
2414
|
+
retrievalStatus: raw.retrievalStatus ?? "unknown"
|
|
2364
2415
|
};
|
|
2365
2416
|
}
|
|
2366
2417
|
|
|
@@ -2470,6 +2521,12 @@ var cdpChatgptAdapter = {
|
|
|
2470
2521
|
model: "chatgpt-web",
|
|
2471
2522
|
groundingSources,
|
|
2472
2523
|
searchQueries: [input.query],
|
|
2524
|
+
// This surface is scraped from the rendered web UI, which exposes no
|
|
2525
|
+
// reliable signal for whether a retrieval step ran behind the answer.
|
|
2526
|
+
// `searchQueries` here is the query we submitted, not evidence of a
|
|
2527
|
+
// search the product performed.
|
|
2528
|
+
retrievalStatus: "unknown",
|
|
2529
|
+
retrievalContract: "native-auto-v1",
|
|
2473
2530
|
screenshotPath: capturedScreenshotPath
|
|
2474
2531
|
};
|
|
2475
2532
|
} catch (err) {
|
|
@@ -2784,7 +2841,14 @@ var perplexityAdapter = {
|
|
|
2784
2841
|
model: raw.model,
|
|
2785
2842
|
servedModel: raw.servedModel,
|
|
2786
2843
|
groundingSources: raw.groundingSources,
|
|
2787
|
-
searchQueries: raw.searchQueries
|
|
2844
|
+
searchQueries: raw.searchQueries,
|
|
2845
|
+
// Retrieval detection is not implemented for this provider. Its candidate
|
|
2846
|
+
// marker is present on 100% of stored rows, so it has never been shown to
|
|
2847
|
+
// discriminate a non-retrieving answer and wiring it up would hardcode
|
|
2848
|
+
// `used`. `unknown` states what we actually know. The contract is a
|
|
2849
|
+
// declaration about how we build the request, so it is always knowable.
|
|
2850
|
+
retrievalStatus: "unknown",
|
|
2851
|
+
retrievalContract: "native-auto-v1"
|
|
2788
2852
|
};
|
|
2789
2853
|
},
|
|
2790
2854
|
normalizeResult(raw) {
|
|
@@ -2801,7 +2865,8 @@ var perplexityAdapter = {
|
|
|
2801
2865
|
answerText: normalized.answerText,
|
|
2802
2866
|
citedDomains: normalized.citedDomains,
|
|
2803
2867
|
groundingSources: normalized.groundingSources,
|
|
2804
|
-
searchQueries: normalized.searchQueries
|
|
2868
|
+
searchQueries: normalized.searchQueries,
|
|
2869
|
+
retrievalStatus: "unknown"
|
|
2805
2870
|
};
|
|
2806
2871
|
},
|
|
2807
2872
|
async generateText(prompt, config) {
|
|
@@ -3620,6 +3685,11 @@ var JobRunner = class {
|
|
|
3620
3685
|
sourceCount: citedUrlCapture.sourceCount,
|
|
3621
3686
|
resolvedCount: citedUrlCapture.resolvedCount,
|
|
3622
3687
|
captureVersion: citedUrlCapture.captureVersion,
|
|
3688
|
+
// Retrieval is the adapter's own observation, never inferred
|
|
3689
|
+
// from citation counts. Both branches record it so no snapshot
|
|
3690
|
+
// can be written unmarked.
|
|
3691
|
+
retrievalStatus: normalized.retrievalStatus,
|
|
3692
|
+
retrievalContract: raw.retrievalContract,
|
|
3623
3693
|
competitorOverlap: overlap,
|
|
3624
3694
|
recommendedCompetitors: extractedCompetitors,
|
|
3625
3695
|
location: runLocation?.label ?? null,
|
|
@@ -3651,6 +3721,11 @@ var JobRunner = class {
|
|
|
3651
3721
|
sourceCount: citedUrlCapture.sourceCount,
|
|
3652
3722
|
resolvedCount: citedUrlCapture.resolvedCount,
|
|
3653
3723
|
captureVersion: citedUrlCapture.captureVersion,
|
|
3724
|
+
// Retrieval is the adapter's own observation, never inferred
|
|
3725
|
+
// from citation counts. Both branches record it so no snapshot
|
|
3726
|
+
// can be written unmarked.
|
|
3727
|
+
retrievalStatus: normalized.retrievalStatus,
|
|
3728
|
+
retrievalContract: raw.retrievalContract,
|
|
3654
3729
|
competitorOverlap: overlap,
|
|
3655
3730
|
recommendedCompetitors: extractedCompetitors,
|
|
3656
3731
|
location: runLocation?.label ?? null,
|
|
@@ -6087,155 +6162,10 @@ function computeSummary2(rows) {
|
|
|
6087
6162
|
return computeBacklinkSummaryMetrics(rows);
|
|
6088
6163
|
}
|
|
6089
6164
|
|
|
6090
|
-
// src/
|
|
6165
|
+
// src/discovery-run.ts
|
|
6091
6166
|
import crypto13 from "crypto";
|
|
6092
6167
|
import { and as and9, eq as eq11 } from "drizzle-orm";
|
|
6093
|
-
var log11 = createLogger("
|
|
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");
|
|
6168
|
+
var log11 = createLogger("DiscoveryRun");
|
|
6239
6169
|
var DEFAULT_SEED_COUNT = 30;
|
|
6240
6170
|
var EMBED_RETRY_MAX_RETRIES = 3;
|
|
6241
6171
|
var EMBED_RETRY_MAX_DELAY_MS = 1e4;
|
|
@@ -6268,7 +6198,7 @@ async function embedWithRetry(fn, opts = {}) {
|
|
|
6268
6198
|
maxDelayMs: EMBED_RETRY_MAX_DELAY_MS,
|
|
6269
6199
|
sleep: opts.sleep,
|
|
6270
6200
|
onRetry: opts.onRetry ?? (({ attempt, delayMs }) => {
|
|
6271
|
-
|
|
6201
|
+
log11.warn("discovery.embed.retry", { attempt, delayMs });
|
|
6272
6202
|
})
|
|
6273
6203
|
}
|
|
6274
6204
|
);
|
|
@@ -6276,11 +6206,11 @@ async function embedWithRetry(fn, opts = {}) {
|
|
|
6276
6206
|
var QUERIES_PER_INTENT_BUCKET = 6;
|
|
6277
6207
|
async function executeDiscoveryRun(opts) {
|
|
6278
6208
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6279
|
-
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
6209
|
+
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, opts.runId)).run();
|
|
6280
6210
|
try {
|
|
6281
|
-
const projectRow = opts.db.select().from(projects).where(
|
|
6211
|
+
const projectRow = opts.db.select().from(projects).where(eq11(projects.id, opts.projectId)).get();
|
|
6282
6212
|
if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
|
|
6283
|
-
const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
6213
|
+
const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
|
|
6284
6214
|
const canonicalDomains = effectiveDomains({
|
|
6285
6215
|
canonicalDomain: projectRow.canonicalDomain,
|
|
6286
6216
|
ownedDomains: projectRow.ownedDomains
|
|
@@ -6318,8 +6248,8 @@ async function executeDiscoveryRun(opts) {
|
|
|
6318
6248
|
seedProvider: result.seedProvider,
|
|
6319
6249
|
result
|
|
6320
6250
|
});
|
|
6321
|
-
opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
6322
|
-
|
|
6251
|
+
opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq11(runs.id, opts.runId)).run();
|
|
6252
|
+
log11.info("discovery.completed", {
|
|
6323
6253
|
runId: opts.runId,
|
|
6324
6254
|
sessionId: opts.sessionId,
|
|
6325
6255
|
buckets: result.buckets,
|
|
@@ -6327,13 +6257,13 @@ async function executeDiscoveryRun(opts) {
|
|
|
6327
6257
|
});
|
|
6328
6258
|
} catch (err) {
|
|
6329
6259
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
6330
|
-
|
|
6260
|
+
log11.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
|
|
6331
6261
|
markSessionFailed(opts.db, opts.sessionId, errorMsg);
|
|
6332
6262
|
opts.db.update(runs).set({
|
|
6333
6263
|
status: RunStatuses.failed,
|
|
6334
6264
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6335
6265
|
error: errorMsg
|
|
6336
|
-
}).where(
|
|
6266
|
+
}).where(eq11(runs.id, opts.runId)).run();
|
|
6337
6267
|
}
|
|
6338
6268
|
}
|
|
6339
6269
|
function buildDefaultDeps(registry) {
|
|
@@ -6582,13 +6512,13 @@ function writeDiscoveryInsight(db, input) {
|
|
|
6582
6512
|
totalProbes
|
|
6583
6513
|
});
|
|
6584
6514
|
db.transaction((tx) => {
|
|
6585
|
-
tx.update(insights).set({ dismissed: true }).where(
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6515
|
+
tx.update(insights).set({ dismissed: true }).where(and9(
|
|
6516
|
+
eq11(insights.projectId, input.projectId),
|
|
6517
|
+
eq11(insights.type, "discovery.basket-divergence"),
|
|
6518
|
+
eq11(insights.dismissed, false)
|
|
6589
6519
|
)).run();
|
|
6590
6520
|
tx.insert(insights).values({
|
|
6591
|
-
id:
|
|
6521
|
+
id: crypto13.randomUUID(),
|
|
6592
6522
|
projectId: input.projectId,
|
|
6593
6523
|
runId: input.runId,
|
|
6594
6524
|
type: "discovery.basket-divergence",
|
|
@@ -6624,10 +6554,10 @@ function buildDiscoveryInsightTitle(input) {
|
|
|
6624
6554
|
}
|
|
6625
6555
|
|
|
6626
6556
|
// src/execute-site-audit.ts
|
|
6627
|
-
import
|
|
6628
|
-
import { eq as
|
|
6557
|
+
import crypto14 from "crypto";
|
|
6558
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6629
6559
|
import { runSitemapAudit } from "@ainyc/aeo-audit";
|
|
6630
|
-
var
|
|
6560
|
+
var log12 = createLogger("SiteAudit");
|
|
6631
6561
|
var SITE_AUDIT_DEFAULT_PAGE_LIMIT = 500;
|
|
6632
6562
|
var SITE_AUDIT_MAX_PAGE_LIMIT = 2e3;
|
|
6633
6563
|
function toHomepageUrl(canonicalDomain) {
|
|
@@ -6688,15 +6618,15 @@ function computeFactorAverages(pages) {
|
|
|
6688
6618
|
}
|
|
6689
6619
|
async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
6690
6620
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6691
|
-
db.update(runs).set({ status: "running", startedAt }).where(
|
|
6621
|
+
db.update(runs).set({ status: "running", startedAt }).where(eq12(runs.id, runId)).run();
|
|
6692
6622
|
try {
|
|
6693
|
-
const project = db.select().from(projects).where(
|
|
6623
|
+
const project = db.select().from(projects).where(eq12(projects.id, projectId)).get();
|
|
6694
6624
|
if (!project) {
|
|
6695
6625
|
throw new Error(`Project not found: ${projectId}`);
|
|
6696
6626
|
}
|
|
6697
6627
|
const homepageUrl = toHomepageUrl(project.canonicalDomain);
|
|
6698
6628
|
const limit = clampSiteAuditLimit(opts.limit);
|
|
6699
|
-
|
|
6629
|
+
log12.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
|
|
6700
6630
|
await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
|
|
6701
6631
|
if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
|
|
6702
6632
|
const report = await runSitemapAudit(homepageUrl, { sitemapUrl: opts.sitemapUrl, limit });
|
|
@@ -6704,7 +6634,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6704
6634
|
const pagesErrored = report.pages.filter((page) => page.status === "error").length;
|
|
6705
6635
|
const auditable = report.pagesDiscovered - report.pagesSkipped;
|
|
6706
6636
|
if (auditable > report.pagesAudited) {
|
|
6707
|
-
|
|
6637
|
+
log12.info("truncated", {
|
|
6708
6638
|
runId,
|
|
6709
6639
|
projectId,
|
|
6710
6640
|
auditable,
|
|
@@ -6723,7 +6653,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6723
6653
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6724
6654
|
db.transaction((tx) => {
|
|
6725
6655
|
tx.insert(siteAuditSnapshots).values({
|
|
6726
|
-
id:
|
|
6656
|
+
id: crypto14.randomUUID(),
|
|
6727
6657
|
projectId,
|
|
6728
6658
|
runId,
|
|
6729
6659
|
sitemapUrl: report.sitemapUrl,
|
|
@@ -6752,7 +6682,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6752
6682
|
}).run();
|
|
6753
6683
|
for (const page of report.pages) {
|
|
6754
6684
|
tx.insert(siteAuditPages).values({
|
|
6755
|
-
id:
|
|
6685
|
+
id: crypto14.randomUUID(),
|
|
6756
6686
|
projectId,
|
|
6757
6687
|
runId,
|
|
6758
6688
|
url: page.url,
|
|
@@ -6763,9 +6693,9 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6763
6693
|
createdAt: finishedAt
|
|
6764
6694
|
}).run();
|
|
6765
6695
|
}
|
|
6766
|
-
tx.update(runs).set({ status, finishedAt }).where(
|
|
6696
|
+
tx.update(runs).set({ status, finishedAt }).where(eq12(runs.id, runId)).run();
|
|
6767
6697
|
});
|
|
6768
|
-
|
|
6698
|
+
log12.info("completed", {
|
|
6769
6699
|
runId,
|
|
6770
6700
|
projectId,
|
|
6771
6701
|
status,
|
|
@@ -6775,14 +6705,14 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6775
6705
|
});
|
|
6776
6706
|
} catch (err) {
|
|
6777
6707
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
6778
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
6779
|
-
|
|
6708
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, runId)).run();
|
|
6709
|
+
log12.error("failed", { runId, projectId, error: errorMsg });
|
|
6780
6710
|
throw err;
|
|
6781
6711
|
}
|
|
6782
6712
|
}
|
|
6783
6713
|
|
|
6784
6714
|
// src/commands/backfill.ts
|
|
6785
|
-
import { and as
|
|
6715
|
+
import { and as and10, eq as eq13, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
|
|
6786
6716
|
var SNAPSHOT_BATCH_SIZE = 500;
|
|
6787
6717
|
async function backfillAnswerVisibilityCommand(opts) {
|
|
6788
6718
|
const config = loadConfig();
|
|
@@ -6790,7 +6720,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6790
6720
|
migrate(db);
|
|
6791
6721
|
const projectFilter = opts?.project?.trim();
|
|
6792
6722
|
const isDryRun = opts?.dryRun === true;
|
|
6793
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
6723
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
6794
6724
|
let examined = 0;
|
|
6795
6725
|
let updated = 0;
|
|
6796
6726
|
let wouldUpdate = 0;
|
|
@@ -6798,10 +6728,10 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6798
6728
|
let reparsed = 0;
|
|
6799
6729
|
let providerErrors = 0;
|
|
6800
6730
|
if (scopedProjects.length > 0) {
|
|
6801
|
-
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
6802
|
-
|
|
6731
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and10(
|
|
6732
|
+
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
6803
6733
|
inArray4(runs.projectId, scopedProjects.map((project) => project.id))
|
|
6804
|
-
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
6734
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq13(runs.kind, RunKinds["answer-visibility"])).all();
|
|
6805
6735
|
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
6806
6736
|
for (const run of runRows) {
|
|
6807
6737
|
const existing = runIdsByProject.get(run.projectId);
|
|
@@ -6809,7 +6739,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6809
6739
|
else runIdsByProject.set(run.projectId, [run.id]);
|
|
6810
6740
|
}
|
|
6811
6741
|
for (const project of scopedProjects) {
|
|
6812
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
6742
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
6813
6743
|
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
6814
6744
|
if (runIds.length === 0) continue;
|
|
6815
6745
|
const projectDomains = effectiveDomains({
|
|
@@ -6855,7 +6785,12 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6855
6785
|
answerText,
|
|
6856
6786
|
citedDomains: reparsedResult.citedDomains,
|
|
6857
6787
|
groundingSources: reparsedResult.groundingSources,
|
|
6858
|
-
searchQueries: reparsedResult.searchQueries
|
|
6788
|
+
searchQueries: reparsedResult.searchQueries,
|
|
6789
|
+
// Backfill recomputes mention and citation state from stored
|
|
6790
|
+
// rows; it never observes retrieval and never writes it back
|
|
6791
|
+
// (nextPatch carries no retrieval field). `unknown` keeps this
|
|
6792
|
+
// reconstruction from asserting anything about a search.
|
|
6793
|
+
retrievalStatus: "unknown"
|
|
6859
6794
|
};
|
|
6860
6795
|
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
6861
6796
|
const nextCitedDomains = reparsedResult.citedDomains;
|
|
@@ -6897,7 +6832,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6897
6832
|
} else {
|
|
6898
6833
|
db.transaction((tx) => {
|
|
6899
6834
|
for (const update of pendingUpdates) {
|
|
6900
|
-
tx.update(querySnapshots).set(update.patch).where(
|
|
6835
|
+
tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
|
|
6901
6836
|
}
|
|
6902
6837
|
});
|
|
6903
6838
|
updated += pendingUpdates.length;
|
|
@@ -6946,13 +6881,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
6946
6881
|
function backfillNormalizedPaths(db, opts) {
|
|
6947
6882
|
const baseConditions = [];
|
|
6948
6883
|
if (opts?.projectId) {
|
|
6949
|
-
baseConditions.push(
|
|
6884
|
+
baseConditions.push(eq13(gaTrafficSnapshots.projectId, opts.projectId));
|
|
6950
6885
|
}
|
|
6951
6886
|
const rows = db.select({
|
|
6952
6887
|
id: gaTrafficSnapshots.id,
|
|
6953
6888
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
6954
6889
|
landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
|
|
6955
|
-
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ?
|
|
6890
|
+
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
|
|
6956
6891
|
let updated = 0;
|
|
6957
6892
|
let unchanged = 0;
|
|
6958
6893
|
if (rows.length > 0) {
|
|
@@ -6967,7 +6902,7 @@ function backfillNormalizedPaths(db, opts) {
|
|
|
6967
6902
|
unchanged++;
|
|
6968
6903
|
continue;
|
|
6969
6904
|
}
|
|
6970
|
-
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(
|
|
6905
|
+
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq13(gaTrafficSnapshots.id, row.id)).run();
|
|
6971
6906
|
updated++;
|
|
6972
6907
|
}
|
|
6973
6908
|
});
|
|
@@ -6981,7 +6916,7 @@ async function backfillNormalizedPathsCommand(opts) {
|
|
|
6981
6916
|
const projectFilter = opts?.project?.trim();
|
|
6982
6917
|
let projectId;
|
|
6983
6918
|
if (projectFilter) {
|
|
6984
|
-
const project = db.select({ id: projects.id }).from(projects).where(
|
|
6919
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
|
|
6985
6920
|
if (!project) {
|
|
6986
6921
|
const result2 = {
|
|
6987
6922
|
project: projectFilter,
|
|
@@ -7018,13 +6953,13 @@ async function backfillNormalizedPathsCommand(opts) {
|
|
|
7018
6953
|
function backfillAiReferralPaths(db, opts) {
|
|
7019
6954
|
const baseConditions = [];
|
|
7020
6955
|
if (opts?.projectId) {
|
|
7021
|
-
baseConditions.push(
|
|
6956
|
+
baseConditions.push(eq13(gaAiReferrals.projectId, opts.projectId));
|
|
7022
6957
|
}
|
|
7023
6958
|
const rows = db.select({
|
|
7024
6959
|
id: gaAiReferrals.id,
|
|
7025
6960
|
landingPage: gaAiReferrals.landingPage,
|
|
7026
6961
|
landingPageNormalized: gaAiReferrals.landingPageNormalized
|
|
7027
|
-
}).from(gaAiReferrals).where(baseConditions.length > 0 ?
|
|
6962
|
+
}).from(gaAiReferrals).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
|
|
7028
6963
|
let updated = 0;
|
|
7029
6964
|
let unchanged = 0;
|
|
7030
6965
|
if (rows.length > 0) {
|
|
@@ -7039,7 +6974,7 @@ function backfillAiReferralPaths(db, opts) {
|
|
|
7039
6974
|
unchanged++;
|
|
7040
6975
|
continue;
|
|
7041
6976
|
}
|
|
7042
|
-
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(
|
|
6977
|
+
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq13(gaAiReferrals.id, row.id)).run();
|
|
7043
6978
|
updated++;
|
|
7044
6979
|
}
|
|
7045
6980
|
});
|
|
@@ -7053,7 +6988,7 @@ async function backfillAiReferralPathsCommand(opts) {
|
|
|
7053
6988
|
const projectFilter = opts?.project?.trim();
|
|
7054
6989
|
let projectId;
|
|
7055
6990
|
if (projectFilter) {
|
|
7056
|
-
const project = db.select({ id: projects.id }).from(projects).where(
|
|
6991
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
|
|
7057
6992
|
if (!project) {
|
|
7058
6993
|
const result2 = {
|
|
7059
6994
|
project: projectFilter,
|
|
@@ -7089,10 +7024,10 @@ async function backfillAiReferralPathsCommand(opts) {
|
|
|
7089
7024
|
}
|
|
7090
7025
|
function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
7091
7026
|
const isDryRun = opts?.dryRun === true;
|
|
7092
|
-
const project = db.select().from(projects).where(
|
|
7027
|
+
const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
|
|
7093
7028
|
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
7094
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
7095
|
-
const runRows = db.select({ id: runs.id }).from(runs).where(
|
|
7029
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
7030
|
+
const runRows = db.select({ id: runs.id }).from(runs).where(and10(eq13(runs.kind, RunKinds["answer-visibility"]), eq13(runs.projectId, projectId))).all();
|
|
7096
7031
|
const runIds = runRows.map((r) => r.id);
|
|
7097
7032
|
let examined = 0;
|
|
7098
7033
|
let updated = 0;
|
|
@@ -7134,6 +7069,8 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
7134
7069
|
answerText,
|
|
7135
7070
|
citedDomains,
|
|
7136
7071
|
groundingSources,
|
|
7072
|
+
// See the note above: retrieval is not observed or written here.
|
|
7073
|
+
retrievalStatus: "unknown",
|
|
7137
7074
|
searchQueries: []
|
|
7138
7075
|
};
|
|
7139
7076
|
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
@@ -7164,7 +7101,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
7164
7101
|
} else {
|
|
7165
7102
|
db.transaction((tx) => {
|
|
7166
7103
|
for (const update of pendingUpdates) {
|
|
7167
|
-
tx.update(querySnapshots).set(update.patch).where(
|
|
7104
|
+
tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
|
|
7168
7105
|
}
|
|
7169
7106
|
});
|
|
7170
7107
|
updated += pendingUpdates.length;
|
|
@@ -7179,7 +7116,7 @@ async function backfillAnswerMentionsCommand(opts) {
|
|
|
7179
7116
|
migrate(db);
|
|
7180
7117
|
const projectFilter = opts?.project?.trim();
|
|
7181
7118
|
const isDryRun = opts?.dryRun === true;
|
|
7182
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
7119
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
7183
7120
|
let examined = 0;
|
|
7184
7121
|
let updated = 0;
|
|
7185
7122
|
let wouldUpdate = 0;
|
|
@@ -7239,7 +7176,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
7239
7176
|
return result;
|
|
7240
7177
|
}
|
|
7241
7178
|
async function backfillInsightsCommand(project, opts) {
|
|
7242
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
7179
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SMYVN3KN.js");
|
|
7243
7180
|
const config = loadConfig();
|
|
7244
7181
|
const db = createClient(config.database);
|
|
7245
7182
|
migrate(db);
|
|
@@ -7398,7 +7335,7 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7398
7335
|
const config = loadConfig();
|
|
7399
7336
|
const db = createClient(config.database);
|
|
7400
7337
|
migrate(db);
|
|
7401
|
-
const project = db.select().from(projects).where(
|
|
7338
|
+
const project = db.select().from(projects).where(eq13(projects.name, opts.project)).get();
|
|
7402
7339
|
if (!project) {
|
|
7403
7340
|
throw new Error(`Project "${opts.project}" not found`);
|
|
7404
7341
|
}
|
|
@@ -7409,8 +7346,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7409
7346
|
process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
|
|
7410
7347
|
`);
|
|
7411
7348
|
}
|
|
7412
|
-
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(
|
|
7413
|
-
|
|
7349
|
+
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and10(
|
|
7350
|
+
eq13(auditLog.projectId, project.id),
|
|
7414
7351
|
inArray4(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
|
|
7415
7352
|
)).orderBy(auditLog.createdAt).all();
|
|
7416
7353
|
const history = replayQueryAuditLog(events);
|
|
@@ -7418,8 +7355,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7418
7355
|
runId: runs.id,
|
|
7419
7356
|
createdAt: runs.createdAt,
|
|
7420
7357
|
location: runs.location
|
|
7421
|
-
}).from(runs).innerJoin(querySnapshots,
|
|
7422
|
-
|
|
7358
|
+
}).from(runs).innerJoin(querySnapshots, eq13(querySnapshots.runId, runs.id)).where(and10(
|
|
7359
|
+
eq13(runs.projectId, project.id),
|
|
7423
7360
|
isNull(querySnapshots.queryId),
|
|
7424
7361
|
isNull(querySnapshots.queryText)
|
|
7425
7362
|
)).groupBy(runs.id).orderBy(runs.createdAt).all();
|
|
@@ -7441,8 +7378,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7441
7378
|
provider: querySnapshots.provider,
|
|
7442
7379
|
createdAt: querySnapshots.createdAt,
|
|
7443
7380
|
answerText: querySnapshots.answerText
|
|
7444
|
-
}).from(querySnapshots).where(
|
|
7445
|
-
|
|
7381
|
+
}).from(querySnapshots).where(and10(
|
|
7382
|
+
eq13(querySnapshots.runId, run.runId),
|
|
7446
7383
|
isNull(querySnapshots.queryId),
|
|
7447
7384
|
isNull(querySnapshots.queryText)
|
|
7448
7385
|
)).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
|
|
@@ -7508,7 +7445,7 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7508
7445
|
if (!isDryRun && updates.length > 0) {
|
|
7509
7446
|
db.transaction((tx) => {
|
|
7510
7447
|
for (const u of updates) {
|
|
7511
|
-
tx.update(querySnapshots).set({ queryText: u.queryText }).where(
|
|
7448
|
+
tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq13(querySnapshots.id, u.id)).run();
|
|
7512
7449
|
}
|
|
7513
7450
|
});
|
|
7514
7451
|
}
|
|
@@ -7582,7 +7519,7 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7582
7519
|
const projectFilter = opts?.project?.trim();
|
|
7583
7520
|
const isDryRun = opts?.dryRun === true;
|
|
7584
7521
|
const isJson = isMachineFormat(opts?.format);
|
|
7585
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
7522
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
7586
7523
|
if (scopedProjects.length === 0) {
|
|
7587
7524
|
if (projectFilter && !isJson) {
|
|
7588
7525
|
process.stderr.write(`No project named "${projectFilter}".
|
|
@@ -7607,8 +7544,8 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7607
7544
|
dryRun: isDryRun,
|
|
7608
7545
|
byBot: {}
|
|
7609
7546
|
};
|
|
7610
|
-
const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(
|
|
7611
|
-
|
|
7547
|
+
const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
|
|
7548
|
+
eq13(rawEventSamples.eventType, "unknown"),
|
|
7612
7549
|
inArray4(rawEventSamples.projectId, projectIds)
|
|
7613
7550
|
)).get();
|
|
7614
7551
|
result.unknownBefore = Number(unknownCountRow?.n ?? 0);
|
|
@@ -7620,8 +7557,8 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7620
7557
|
userAgent: rawEventSamples.userAgent,
|
|
7621
7558
|
pathNormalized: rawEventSamples.pathNormalized,
|
|
7622
7559
|
status: rawEventSamples.status
|
|
7623
|
-
}).from(rawEventSamples).where(
|
|
7624
|
-
|
|
7560
|
+
}).from(rawEventSamples).where(and10(
|
|
7561
|
+
eq13(rawEventSamples.eventType, "unknown"),
|
|
7625
7562
|
inArray4(rawEventSamples.projectId, projectIds)
|
|
7626
7563
|
)).all();
|
|
7627
7564
|
result.examined = unknownSamples.length;
|
|
@@ -7660,7 +7597,7 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7660
7597
|
result.reclassified++;
|
|
7661
7598
|
result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
|
|
7662
7599
|
if (isDryRun) continue;
|
|
7663
|
-
db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(
|
|
7600
|
+
db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq13(rawEventSamples.id, snap.id)).run();
|
|
7664
7601
|
const tsHour = new Date(snap.ts);
|
|
7665
7602
|
tsHour.setUTCMinutes(0, 0, 0);
|
|
7666
7603
|
if (userFetch) {
|
|
@@ -7724,8 +7661,8 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7724
7661
|
}
|
|
7725
7662
|
}
|
|
7726
7663
|
if (!isDryRun) {
|
|
7727
|
-
const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(
|
|
7728
|
-
|
|
7664
|
+
const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
|
|
7665
|
+
eq13(rawEventSamples.eventType, "unknown"),
|
|
7729
7666
|
inArray4(rawEventSamples.projectId, projectIds)
|
|
7730
7667
|
)).get();
|
|
7731
7668
|
result.unknownAfter = Number(afterRow?.n ?? 0);
|
|
@@ -7760,7 +7697,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
7760
7697
|
}
|
|
7761
7698
|
|
|
7762
7699
|
// src/commands/skills.ts
|
|
7763
|
-
import
|
|
7700
|
+
import crypto15 from "crypto";
|
|
7764
7701
|
import fs4 from "fs";
|
|
7765
7702
|
import os4 from "os";
|
|
7766
7703
|
import path5 from "path";
|
|
@@ -7815,7 +7752,7 @@ function walkRelative(dir, prefix = "") {
|
|
|
7815
7752
|
return out.sort();
|
|
7816
7753
|
}
|
|
7817
7754
|
function sha256File(filePath) {
|
|
7818
|
-
return
|
|
7755
|
+
return crypto15.createHash("sha256").update(fs4.readFileSync(filePath)).digest("hex");
|
|
7819
7756
|
}
|
|
7820
7757
|
function readSkillManifest(skillDir) {
|
|
7821
7758
|
try {
|
|
@@ -8157,10 +8094,11 @@ var ProviderRegistry = class {
|
|
|
8157
8094
|
};
|
|
8158
8095
|
|
|
8159
8096
|
// src/scheduler.ts
|
|
8160
|
-
import
|
|
8097
|
+
import crypto16 from "crypto";
|
|
8161
8098
|
import cron from "node-cron";
|
|
8162
|
-
import { and as
|
|
8163
|
-
var
|
|
8099
|
+
import { and as and11, eq as eq14, inArray as inArray5, notExists, sql as sql6 } from "drizzle-orm";
|
|
8100
|
+
var log13 = createLogger("Scheduler");
|
|
8101
|
+
var DEFAULT_HEALTH_CRON = "0 */6 * * *";
|
|
8164
8102
|
function taskKey(projectId, kind) {
|
|
8165
8103
|
return `${projectId}::${kind}`;
|
|
8166
8104
|
}
|
|
@@ -8172,18 +8110,60 @@ var Scheduler = class {
|
|
|
8172
8110
|
this.db = db;
|
|
8173
8111
|
this.callbacks = callbacks;
|
|
8174
8112
|
}
|
|
8113
|
+
/**
|
|
8114
|
+
* Give every project a health schedule if it has never had one.
|
|
8115
|
+
*
|
|
8116
|
+
* Ships the schedule with the feature. The six checks that existed before
|
|
8117
|
+
* this had never run on a live instance because nothing scheduled them, and
|
|
8118
|
+
* a health check nobody enables is indistinguishable from no health check.
|
|
8119
|
+
*
|
|
8120
|
+
* Keyed on the row EXISTING, not on it being enabled, so an operator who
|
|
8121
|
+
* deliberately turns this off keeps it off across restarts. Seeded here
|
|
8122
|
+
* rather than in a migration because migrations must stay additive to remain
|
|
8123
|
+
* downgrade-safe, and inserting rows is a data mutation.
|
|
8124
|
+
*
|
|
8125
|
+
* Every 6h, not daily: the cadence has to be shorter than the time-to-damage
|
|
8126
|
+
* it guards. A Vercel source begins discarding traffic once it is 24h behind,
|
|
8127
|
+
* so a daily pass could only ever observe the loss after it started.
|
|
8128
|
+
*/
|
|
8129
|
+
ensureHealthSchedules() {
|
|
8130
|
+
const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
|
|
8131
|
+
this.db.select({ one: sql6`1` }).from(schedules).where(and11(
|
|
8132
|
+
eq14(schedules.projectId, projects.id),
|
|
8133
|
+
eq14(schedules.kind, SchedulableRunKinds.doctor)
|
|
8134
|
+
))
|
|
8135
|
+
)).all();
|
|
8136
|
+
if (projectsWithoutHealth.length === 0) return;
|
|
8137
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8138
|
+
for (const project of projectsWithoutHealth) {
|
|
8139
|
+
this.db.insert(schedules).values({
|
|
8140
|
+
id: crypto16.randomUUID(),
|
|
8141
|
+
projectId: project.id,
|
|
8142
|
+
kind: SchedulableRunKinds.doctor,
|
|
8143
|
+
cronExpr: DEFAULT_HEALTH_CRON,
|
|
8144
|
+
timezone: "UTC",
|
|
8145
|
+
enabled: true,
|
|
8146
|
+
providers: [],
|
|
8147
|
+
nextRunAt: nextRunFromCron(DEFAULT_HEALTH_CRON, "UTC"),
|
|
8148
|
+
createdAt: now,
|
|
8149
|
+
updatedAt: now
|
|
8150
|
+
}).run();
|
|
8151
|
+
}
|
|
8152
|
+
log13.info("health-schedule.seeded", { projectCount: projectsWithoutHealth.length, cron: DEFAULT_HEALTH_CRON });
|
|
8153
|
+
}
|
|
8175
8154
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
8176
8155
|
start() {
|
|
8177
|
-
|
|
8156
|
+
this.ensureHealthSchedules();
|
|
8157
|
+
const allSchedules = this.db.select().from(schedules).where(eq14(schedules.enabled, true)).all();
|
|
8178
8158
|
for (const schedule of allSchedules) {
|
|
8179
8159
|
const missedRunAt = schedule.nextRunAt;
|
|
8180
8160
|
this.registerCronTask(schedule);
|
|
8181
8161
|
if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
|
|
8182
|
-
|
|
8162
|
+
log13.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
|
|
8183
8163
|
this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
|
|
8184
8164
|
}
|
|
8185
8165
|
}
|
|
8186
|
-
|
|
8166
|
+
log13.info("started", { scheduleCount: allSchedules.length });
|
|
8187
8167
|
}
|
|
8188
8168
|
/** Stop all cron tasks for graceful shutdown. */
|
|
8189
8169
|
stop() {
|
|
@@ -8204,7 +8184,7 @@ var Scheduler = class {
|
|
|
8204
8184
|
this.stopTask(key, existing, "Stopped");
|
|
8205
8185
|
this.tasks.delete(key);
|
|
8206
8186
|
}
|
|
8207
|
-
const schedule = this.db.select().from(schedules).where(
|
|
8187
|
+
const schedule = this.db.select().from(schedules).where(and11(eq14(schedules.projectId, projectId), eq14(schedules.kind, kind))).get();
|
|
8208
8188
|
if (schedule && schedule.enabled) {
|
|
8209
8189
|
this.registerCronTask(schedule);
|
|
8210
8190
|
}
|
|
@@ -8227,13 +8207,13 @@ var Scheduler = class {
|
|
|
8227
8207
|
stopTask(key, task, verb) {
|
|
8228
8208
|
void task.stop();
|
|
8229
8209
|
void task.destroy();
|
|
8230
|
-
|
|
8210
|
+
log13.info(`task.${verb.toLowerCase()}`, { key });
|
|
8231
8211
|
}
|
|
8232
8212
|
registerCronTask(schedule) {
|
|
8233
8213
|
const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
|
|
8234
8214
|
const kind = schedule.kind;
|
|
8235
8215
|
if (!cron.validate(cronExpr)) {
|
|
8236
|
-
|
|
8216
|
+
log13.error("cron.invalid", { projectId, kind, cronExpr });
|
|
8237
8217
|
return;
|
|
8238
8218
|
}
|
|
8239
8219
|
const task = cron.schedule(cronExpr, () => {
|
|
@@ -8245,51 +8225,51 @@ var Scheduler = class {
|
|
|
8245
8225
|
this.db.update(schedules).set({
|
|
8246
8226
|
nextRunAt: nextRunFromCron(cronExpr, timezone),
|
|
8247
8227
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8248
|
-
}).where(
|
|
8228
|
+
}).where(eq14(schedules.id, scheduleId)).run();
|
|
8249
8229
|
const label = schedule.preset ?? cronExpr;
|
|
8250
|
-
|
|
8230
|
+
log13.info("cron.registered", { projectId, kind, schedule: label, timezone });
|
|
8251
8231
|
}
|
|
8252
8232
|
triggerRun(scheduleId, projectId, kind) {
|
|
8253
8233
|
try {
|
|
8254
8234
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8255
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
8235
|
+
const currentSchedule = this.db.select().from(schedules).where(eq14(schedules.id, scheduleId)).get();
|
|
8256
8236
|
if (!currentSchedule || !currentSchedule.enabled) {
|
|
8257
|
-
|
|
8237
|
+
log13.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
8258
8238
|
this.remove(projectId, kind);
|
|
8259
8239
|
return;
|
|
8260
8240
|
}
|
|
8261
8241
|
const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
|
|
8262
|
-
const project = this.db.select().from(projects).where(
|
|
8242
|
+
const project = this.db.select().from(projects).where(eq14(projects.id, projectId)).get();
|
|
8263
8243
|
if (!project) {
|
|
8264
|
-
|
|
8244
|
+
log13.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
8265
8245
|
this.remove(projectId, kind);
|
|
8266
8246
|
return;
|
|
8267
8247
|
}
|
|
8268
8248
|
if (kind === SchedulableRunKinds["traffic-sync"]) {
|
|
8269
8249
|
const sourceId = currentSchedule.sourceId;
|
|
8270
8250
|
if (!sourceId) {
|
|
8271
|
-
|
|
8251
|
+
log13.warn("traffic-sync.missing-source", { scheduleId, projectId });
|
|
8272
8252
|
return;
|
|
8273
8253
|
}
|
|
8274
8254
|
if (!this.callbacks.onTrafficSyncRequested) {
|
|
8275
|
-
|
|
8255
|
+
log13.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
|
|
8276
8256
|
return;
|
|
8277
8257
|
}
|
|
8278
8258
|
this.db.update(schedules).set({
|
|
8279
8259
|
lastRunAt: now,
|
|
8280
8260
|
nextRunAt,
|
|
8281
8261
|
updatedAt: now
|
|
8282
|
-
}).where(
|
|
8283
|
-
|
|
8262
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8263
|
+
log13.info("traffic-sync.triggered", { projectName: project.name, sourceId });
|
|
8284
8264
|
this.callbacks.onTrafficSyncRequested(project.name, sourceId);
|
|
8285
8265
|
return;
|
|
8286
8266
|
}
|
|
8287
8267
|
if (kind === SchedulableRunKinds["gbp-sync"]) {
|
|
8288
8268
|
if (!this.callbacks.onGbpSyncRequested) {
|
|
8289
|
-
|
|
8269
|
+
log13.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
|
|
8290
8270
|
return;
|
|
8291
8271
|
}
|
|
8292
|
-
const runId2 =
|
|
8272
|
+
const runId2 = crypto16.randomUUID();
|
|
8293
8273
|
this.db.insert(runs).values({
|
|
8294
8274
|
id: runId2,
|
|
8295
8275
|
projectId,
|
|
@@ -8302,27 +8282,27 @@ var Scheduler = class {
|
|
|
8302
8282
|
lastRunAt: now,
|
|
8303
8283
|
nextRunAt,
|
|
8304
8284
|
updatedAt: now
|
|
8305
|
-
}).where(
|
|
8306
|
-
|
|
8285
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8286
|
+
log13.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
|
|
8307
8287
|
this.callbacks.onGbpSyncRequested(runId2, projectId);
|
|
8308
8288
|
return;
|
|
8309
8289
|
}
|
|
8310
8290
|
if (kind === SchedulableRunKinds["ads-sync"]) {
|
|
8311
8291
|
if (!this.callbacks.onAdsSyncRequested) {
|
|
8312
|
-
|
|
8292
|
+
log13.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
|
|
8313
8293
|
return;
|
|
8314
8294
|
}
|
|
8315
|
-
const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(
|
|
8316
|
-
|
|
8317
|
-
|
|
8295
|
+
const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and11(
|
|
8296
|
+
eq14(runs.projectId, projectId),
|
|
8297
|
+
eq14(runs.kind, RunKinds["ads-sync"]),
|
|
8318
8298
|
inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
8319
8299
|
)).get();
|
|
8320
8300
|
if (activeAdsRun) {
|
|
8321
|
-
|
|
8322
|
-
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(
|
|
8301
|
+
log13.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
|
|
8302
|
+
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8323
8303
|
return;
|
|
8324
8304
|
}
|
|
8325
|
-
const runId2 =
|
|
8305
|
+
const runId2 = crypto16.randomUUID();
|
|
8326
8306
|
this.db.insert(runs).values({
|
|
8327
8307
|
id: runId2,
|
|
8328
8308
|
projectId,
|
|
@@ -8335,55 +8315,69 @@ var Scheduler = class {
|
|
|
8335
8315
|
lastRunAt: now,
|
|
8336
8316
|
nextRunAt,
|
|
8337
8317
|
updatedAt: now
|
|
8338
|
-
}).where(
|
|
8339
|
-
|
|
8318
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8319
|
+
log13.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
|
|
8340
8320
|
this.callbacks.onAdsSyncRequested(runId2, projectId);
|
|
8341
8321
|
return;
|
|
8342
8322
|
}
|
|
8343
8323
|
if (kind === SchedulableRunKinds["data-refresh"]) {
|
|
8344
8324
|
if (!this.callbacks.onDataRefreshRequested) {
|
|
8345
|
-
|
|
8325
|
+
log13.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
|
|
8346
8326
|
return;
|
|
8347
8327
|
}
|
|
8348
8328
|
this.db.update(schedules).set({
|
|
8349
8329
|
lastRunAt: now,
|
|
8350
8330
|
nextRunAt,
|
|
8351
8331
|
updatedAt: now
|
|
8352
|
-
}).where(
|
|
8353
|
-
|
|
8332
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8333
|
+
log13.info("data-refresh.triggered", { projectName: project.name });
|
|
8354
8334
|
this.callbacks.onDataRefreshRequested(project.name);
|
|
8355
8335
|
return;
|
|
8356
8336
|
}
|
|
8337
|
+
if (kind === SchedulableRunKinds.doctor) {
|
|
8338
|
+
if (!this.callbacks.onDoctorRequested) {
|
|
8339
|
+
log13.warn("doctor.no-callback", { scheduleId, projectId, msg: "host did not register onDoctorRequested" });
|
|
8340
|
+
return;
|
|
8341
|
+
}
|
|
8342
|
+
this.db.update(schedules).set({
|
|
8343
|
+
lastRunAt: now,
|
|
8344
|
+
nextRunAt,
|
|
8345
|
+
updatedAt: now
|
|
8346
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8347
|
+
log13.info("doctor.triggered", { projectName: project.name });
|
|
8348
|
+
this.callbacks.onDoctorRequested(project.name);
|
|
8349
|
+
return;
|
|
8350
|
+
}
|
|
8357
8351
|
if (kind === SchedulableRunKinds["backlinks-sync"]) {
|
|
8358
8352
|
if (!this.callbacks.onBacklinksSyncRequested) {
|
|
8359
|
-
|
|
8353
|
+
log13.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
|
|
8360
8354
|
return;
|
|
8361
8355
|
}
|
|
8362
8356
|
this.db.update(schedules).set({
|
|
8363
8357
|
lastRunAt: now,
|
|
8364
8358
|
nextRunAt,
|
|
8365
8359
|
updatedAt: now
|
|
8366
|
-
}).where(
|
|
8367
|
-
|
|
8360
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8361
|
+
log13.info("backlinks-sync.triggered", { projectName: project.name });
|
|
8368
8362
|
this.callbacks.onBacklinksSyncRequested(project.name);
|
|
8369
8363
|
return;
|
|
8370
8364
|
}
|
|
8371
8365
|
if (kind === SchedulableRunKinds["site-audit"]) {
|
|
8372
8366
|
if (!this.callbacks.onSiteAuditRequested) {
|
|
8373
|
-
|
|
8367
|
+
log13.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
|
|
8374
8368
|
return;
|
|
8375
8369
|
}
|
|
8376
|
-
const active = this.db.select({ id: runs.id }).from(runs).where(
|
|
8377
|
-
|
|
8378
|
-
|
|
8370
|
+
const active = this.db.select({ id: runs.id }).from(runs).where(and11(
|
|
8371
|
+
eq14(runs.projectId, projectId),
|
|
8372
|
+
eq14(runs.kind, RunKinds["site-audit"]),
|
|
8379
8373
|
inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
8380
8374
|
)).get();
|
|
8381
8375
|
if (active) {
|
|
8382
|
-
|
|
8383
|
-
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(
|
|
8376
|
+
log13.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
|
|
8377
|
+
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8384
8378
|
return;
|
|
8385
8379
|
}
|
|
8386
|
-
const runId2 =
|
|
8380
|
+
const runId2 = crypto16.randomUUID();
|
|
8387
8381
|
this.db.insert(runs).values({
|
|
8388
8382
|
id: runId2,
|
|
8389
8383
|
projectId,
|
|
@@ -8396,8 +8390,8 @@ var Scheduler = class {
|
|
|
8396
8390
|
lastRunAt: now,
|
|
8397
8391
|
nextRunAt,
|
|
8398
8392
|
updatedAt: now
|
|
8399
|
-
}).where(
|
|
8400
|
-
|
|
8393
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8394
|
+
log13.info("site-audit.triggered", { runId: runId2, projectName: project.name });
|
|
8401
8395
|
this.callbacks.onSiteAuditRequested(runId2, projectId);
|
|
8402
8396
|
return;
|
|
8403
8397
|
}
|
|
@@ -8406,7 +8400,7 @@ var Scheduler = class {
|
|
|
8406
8400
|
if (project.defaultLocation) {
|
|
8407
8401
|
const loc = projectLocations.find((l) => l.label === project.defaultLocation);
|
|
8408
8402
|
if (!loc) {
|
|
8409
|
-
|
|
8403
|
+
log13.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
|
|
8410
8404
|
return;
|
|
8411
8405
|
}
|
|
8412
8406
|
resolvedLocation = loc;
|
|
@@ -8420,11 +8414,11 @@ var Scheduler = class {
|
|
|
8420
8414
|
location: locationLabel
|
|
8421
8415
|
});
|
|
8422
8416
|
if (queueResult.conflict) {
|
|
8423
|
-
|
|
8417
|
+
log13.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
|
|
8424
8418
|
this.db.update(schedules).set({
|
|
8425
8419
|
nextRunAt,
|
|
8426
8420
|
updatedAt: now
|
|
8427
|
-
}).where(
|
|
8421
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8428
8422
|
return;
|
|
8429
8423
|
}
|
|
8430
8424
|
const runId = queueResult.runId;
|
|
@@ -8432,19 +8426,19 @@ var Scheduler = class {
|
|
|
8432
8426
|
lastRunAt: now,
|
|
8433
8427
|
nextRunAt,
|
|
8434
8428
|
updatedAt: now
|
|
8435
|
-
}).where(
|
|
8429
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8436
8430
|
const scheduleProviders = currentSchedule.providers;
|
|
8437
8431
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
8438
|
-
|
|
8432
|
+
log13.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
8439
8433
|
this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
|
|
8440
8434
|
} catch (err) {
|
|
8441
|
-
|
|
8435
|
+
log13.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
|
|
8442
8436
|
}
|
|
8443
8437
|
}
|
|
8444
8438
|
};
|
|
8445
8439
|
|
|
8446
8440
|
// src/data-refresh.ts
|
|
8447
|
-
var
|
|
8441
|
+
var log14 = createLogger("DataRefresh");
|
|
8448
8442
|
async function refreshAllIntegrations(client, projectName) {
|
|
8449
8443
|
const integrations = [
|
|
8450
8444
|
{ name: "gsc", run: () => client.gscSync(projectName, {}) },
|
|
@@ -8457,19 +8451,19 @@ async function refreshAllIntegrations(client, projectName) {
|
|
|
8457
8451
|
results.forEach((result, idx) => {
|
|
8458
8452
|
const integration = integrations[idx].name;
|
|
8459
8453
|
if (result.status === "fulfilled") {
|
|
8460
|
-
|
|
8454
|
+
log14.info("integration.refreshed", { projectName, integration });
|
|
8461
8455
|
} else {
|
|
8462
8456
|
const reason = result.reason;
|
|
8463
8457
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
8464
|
-
|
|
8458
|
+
log14.warn("integration.refresh-failed", { projectName, integration, error: message });
|
|
8465
8459
|
}
|
|
8466
8460
|
});
|
|
8467
8461
|
}
|
|
8468
8462
|
|
|
8469
8463
|
// src/notifier.ts
|
|
8470
|
-
import { eq as
|
|
8471
|
-
import
|
|
8472
|
-
var
|
|
8464
|
+
import { eq as eq15, desc as desc5, and as and12, inArray as inArray6, or } from "drizzle-orm";
|
|
8465
|
+
import crypto17 from "crypto";
|
|
8466
|
+
var log15 = createLogger("Notifier");
|
|
8473
8467
|
var Notifier = class {
|
|
8474
8468
|
db;
|
|
8475
8469
|
serverUrl;
|
|
@@ -8479,26 +8473,26 @@ var Notifier = class {
|
|
|
8479
8473
|
}
|
|
8480
8474
|
/** Called after a run completes (success, partial, or failed). */
|
|
8481
8475
|
async onRunCompleted(runId, projectId) {
|
|
8482
|
-
|
|
8483
|
-
const notifs = this.db.select().from(notifications).where(
|
|
8476
|
+
log15.info("run.completed", { runId, projectId });
|
|
8477
|
+
const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
8484
8478
|
if (notifs.length === 0) {
|
|
8485
|
-
|
|
8479
|
+
log15.info("notifications.none-enabled", { projectId });
|
|
8486
8480
|
return;
|
|
8487
8481
|
}
|
|
8488
|
-
|
|
8489
|
-
const run = this.db.select().from(runs).where(
|
|
8482
|
+
log15.info("notifications.found", { projectId, count: notifs.length });
|
|
8483
|
+
const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
8490
8484
|
if (!run) {
|
|
8491
|
-
|
|
8485
|
+
log15.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
8492
8486
|
return;
|
|
8493
8487
|
}
|
|
8494
|
-
const project = this.db.select().from(projects).where(
|
|
8488
|
+
const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
8495
8489
|
if (!project) {
|
|
8496
|
-
|
|
8490
|
+
log15.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
8497
8491
|
return;
|
|
8498
8492
|
}
|
|
8499
8493
|
const transitions = this.computeTransitions(runId, projectId);
|
|
8500
8494
|
const events = [];
|
|
8501
|
-
|
|
8495
|
+
log15.info("run.status", { runId: run.id, status: run.status, projectId });
|
|
8502
8496
|
if (run.status === "completed" || run.status === "partial") {
|
|
8503
8497
|
events.push("run.completed");
|
|
8504
8498
|
}
|
|
@@ -8514,7 +8508,7 @@ var Notifier = class {
|
|
|
8514
8508
|
if (!config.url) continue;
|
|
8515
8509
|
const subscribedEvents = config.events;
|
|
8516
8510
|
const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
|
|
8517
|
-
|
|
8511
|
+
log15.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
|
|
8518
8512
|
if (matchingEvents.length === 0) continue;
|
|
8519
8513
|
for (const event of matchingEvents) {
|
|
8520
8514
|
const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
|
|
@@ -8530,6 +8524,101 @@ var Notifier = class {
|
|
|
8530
8524
|
}
|
|
8531
8525
|
}
|
|
8532
8526
|
}
|
|
8527
|
+
/**
|
|
8528
|
+
* Record the outcome of a scheduled health pass and notify on transitions.
|
|
8529
|
+
*
|
|
8530
|
+
* Edge-triggered on `(status, code)`. A pass that finds the same problem as
|
|
8531
|
+
* last time is stored but not re-sent: an operator who receives the same
|
|
8532
|
+
* warning every day stops reading the channel, which is how a real
|
|
8533
|
+
* degradation goes unnoticed. A pass whose worst code CHANGES does notify,
|
|
8534
|
+
* even at the same severity, because a different cause is different news.
|
|
8535
|
+
*
|
|
8536
|
+
* Returns the event emitted, or null when nothing changed — the return value
|
|
8537
|
+
* is what tests assert against, so "stayed quiet" is observable rather than
|
|
8538
|
+
* inferred from the absence of a webhook.
|
|
8539
|
+
*/
|
|
8540
|
+
async onHealthChecked(projectId, report) {
|
|
8541
|
+
const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
8542
|
+
if (!project) {
|
|
8543
|
+
log15.error("project.not-found", { projectId, msg: "skipping health notification" });
|
|
8544
|
+
return null;
|
|
8545
|
+
}
|
|
8546
|
+
const rank = (status2) => status2 === "fail" ? 2 : status2 === "warn" ? 1 : 0;
|
|
8547
|
+
const categoryRank = (category) => {
|
|
8548
|
+
switch (category) {
|
|
8549
|
+
case "database":
|
|
8550
|
+
return 6;
|
|
8551
|
+
case "integrations":
|
|
8552
|
+
return 5;
|
|
8553
|
+
case "providers":
|
|
8554
|
+
return 4;
|
|
8555
|
+
case "auth":
|
|
8556
|
+
return 3;
|
|
8557
|
+
case "schedules":
|
|
8558
|
+
return 2;
|
|
8559
|
+
case "config":
|
|
8560
|
+
return 1;
|
|
8561
|
+
default:
|
|
8562
|
+
return 0;
|
|
8563
|
+
}
|
|
8564
|
+
};
|
|
8565
|
+
const graded = report.checks.filter((c) => c.status === "fail" || c.status === "warn" || c.status === "ok");
|
|
8566
|
+
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));
|
|
8567
|
+
const worst = failing.at(0) ?? null;
|
|
8568
|
+
const noSignal = graded.length === 0;
|
|
8569
|
+
const status = noSignal ? "warn" : worst ? worst.status : "ok";
|
|
8570
|
+
const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
|
|
8571
|
+
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.`;
|
|
8572
|
+
const previous = this.db.select().from(doctorHealthState).where(eq15(doctorHealthState.projectId, projectId)).get();
|
|
8573
|
+
const previousStatus = previous?.status ?? null;
|
|
8574
|
+
let event = null;
|
|
8575
|
+
if (previous === void 0) {
|
|
8576
|
+
if (status !== "ok") event = "health.degraded";
|
|
8577
|
+
} else if (status === "ok" && previousStatus !== "ok") {
|
|
8578
|
+
event = "health.recovered";
|
|
8579
|
+
} else if (status !== "ok" && (previousStatus === "ok" || previous.code !== code)) {
|
|
8580
|
+
event = "health.degraded";
|
|
8581
|
+
}
|
|
8582
|
+
const now = report.checkedAt;
|
|
8583
|
+
const observation = { projectId, status, code, summary, checkedAt: now };
|
|
8584
|
+
if (previous === void 0) {
|
|
8585
|
+
this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
|
|
8586
|
+
} else {
|
|
8587
|
+
this.db.update(doctorHealthState).set(observation).where(eq15(doctorHealthState.projectId, projectId)).run();
|
|
8588
|
+
}
|
|
8589
|
+
if (!event) {
|
|
8590
|
+
log15.info("health.unchanged", { projectId, status, code });
|
|
8591
|
+
return null;
|
|
8592
|
+
}
|
|
8593
|
+
const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
8594
|
+
const payload = {
|
|
8595
|
+
source: "canonry",
|
|
8596
|
+
event,
|
|
8597
|
+
project: { name: project.name, canonicalDomain: project.canonicalDomain },
|
|
8598
|
+
health: {
|
|
8599
|
+
status,
|
|
8600
|
+
code,
|
|
8601
|
+
summary,
|
|
8602
|
+
remediation: worst?.remediation ?? null,
|
|
8603
|
+
checkedAt: now,
|
|
8604
|
+
previousStatus,
|
|
8605
|
+
failing: failing.map((c) => ({ id: c.id, status: c.status, code: c.code, summary: c.summary }))
|
|
8606
|
+
},
|
|
8607
|
+
dashboardUrl: `${this.serverUrl}/projects/${project.name}`
|
|
8608
|
+
};
|
|
8609
|
+
let delivered = 0;
|
|
8610
|
+
for (const notif of notifs) {
|
|
8611
|
+
const config = notif.config;
|
|
8612
|
+
if (!config.url) continue;
|
|
8613
|
+
await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
|
|
8614
|
+
delivered += 1;
|
|
8615
|
+
}
|
|
8616
|
+
if (delivered > 0) {
|
|
8617
|
+
this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq15(doctorHealthState.projectId, projectId)).run();
|
|
8618
|
+
}
|
|
8619
|
+
log15.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
|
|
8620
|
+
return event;
|
|
8621
|
+
}
|
|
8533
8622
|
/** Dispatch insight webhooks for critical/high severity insights after a run. */
|
|
8534
8623
|
async dispatchInsightWebhooks(runId, projectId, result) {
|
|
8535
8624
|
const insightEvents = [];
|
|
@@ -8538,11 +8627,11 @@ var Notifier = class {
|
|
|
8538
8627
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
8539
8628
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
8540
8629
|
if (insightEvents.length === 0) return;
|
|
8541
|
-
const notifs = this.db.select().from(notifications).where(
|
|
8630
|
+
const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
8542
8631
|
if (notifs.length === 0) return;
|
|
8543
|
-
const run = this.db.select().from(runs).where(
|
|
8632
|
+
const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
8544
8633
|
if (!run) return;
|
|
8545
|
-
const project = this.db.select().from(projects).where(
|
|
8634
|
+
const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
8546
8635
|
if (!project) return;
|
|
8547
8636
|
for (const notif of notifs) {
|
|
8548
8637
|
const config = notif.config;
|
|
@@ -8572,12 +8661,12 @@ var Notifier = class {
|
|
|
8572
8661
|
}
|
|
8573
8662
|
}
|
|
8574
8663
|
computeTransitions(runId, projectId) {
|
|
8575
|
-
const thisRun = this.db.select().from(runs).where(
|
|
8664
|
+
const thisRun = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
8576
8665
|
if (!thisRun) return [];
|
|
8577
|
-
const groupSiblings = this.db.select().from(runs).where(
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8666
|
+
const groupSiblings = this.db.select().from(runs).where(and12(
|
|
8667
|
+
eq15(runs.projectId, projectId),
|
|
8668
|
+
eq15(runs.kind, thisRun.kind),
|
|
8669
|
+
eq15(runs.createdAt, thisRun.createdAt)
|
|
8581
8670
|
)).all();
|
|
8582
8671
|
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
8583
8672
|
if (stillPending) return [];
|
|
@@ -8593,17 +8682,17 @@ var Notifier = class {
|
|
|
8593
8682
|
return candidate.id > best.id ? candidate : best;
|
|
8594
8683
|
});
|
|
8595
8684
|
if (winner.id !== runId) return [];
|
|
8596
|
-
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
8685
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq15(projects.id, projectId)).get();
|
|
8597
8686
|
const locationCount = Math.max(
|
|
8598
8687
|
1,
|
|
8599
8688
|
(projectLocations?.locations ?? []).length
|
|
8600
8689
|
);
|
|
8601
8690
|
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
8602
8691
|
const recentRuns = this.db.select().from(runs).where(
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
or(
|
|
8692
|
+
and12(
|
|
8693
|
+
eq15(runs.projectId, projectId),
|
|
8694
|
+
eq15(runs.kind, thisRun.kind),
|
|
8695
|
+
or(eq15(runs.status, "completed"), eq15(runs.status, "partial"))
|
|
8607
8696
|
)
|
|
8608
8697
|
).orderBy(desc5(runs.createdAt), desc5(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
8609
8698
|
const groups = groupRunsByCreatedAt(recentRuns);
|
|
@@ -8620,7 +8709,7 @@ var Notifier = class {
|
|
|
8620
8709
|
provider: querySnapshots.provider,
|
|
8621
8710
|
location: querySnapshots.location,
|
|
8622
8711
|
citationState: querySnapshots.citationState
|
|
8623
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8712
|
+
}).from(querySnapshots).leftJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
|
|
8624
8713
|
const previousSnapshots = this.db.select({
|
|
8625
8714
|
queryId: querySnapshots.queryId,
|
|
8626
8715
|
provider: querySnapshots.provider,
|
|
@@ -8650,26 +8739,29 @@ var Notifier = class {
|
|
|
8650
8739
|
return transitions;
|
|
8651
8740
|
}
|
|
8652
8741
|
async sendWebhook(url, payload, notificationId, projectId, webhookSecret) {
|
|
8742
|
+
const destination = resolveDestination(url);
|
|
8743
|
+
const body = destination.render ? destination.render(toAlertView(payload)) : payload;
|
|
8744
|
+
const signingSecret = destination.signed ? webhookSecret : null;
|
|
8653
8745
|
const targetLabel = redactNotificationUrl(url).urlDisplay;
|
|
8654
8746
|
const targetCheck = await resolveWebhookTarget(url);
|
|
8655
8747
|
if (!targetCheck.ok) {
|
|
8656
|
-
|
|
8748
|
+
log15.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
|
|
8657
8749
|
this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
|
|
8658
8750
|
return;
|
|
8659
8751
|
}
|
|
8660
|
-
|
|
8752
|
+
log15.info("webhook.send", { event: payload.event, url: targetLabel });
|
|
8661
8753
|
const maxRetries = 3;
|
|
8662
8754
|
const delays = [1e3, 4e3, 16e3];
|
|
8663
8755
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
8664
8756
|
try {
|
|
8665
|
-
const response = await deliverWebhook(targetCheck.target,
|
|
8757
|
+
const response = await deliverWebhook(targetCheck.target, body, signingSecret);
|
|
8666
8758
|
if (response.status >= 200 && response.status < 300) {
|
|
8667
|
-
|
|
8759
|
+
log15.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
|
|
8668
8760
|
this.logDelivery(projectId, notificationId, payload.event, "sent", null);
|
|
8669
8761
|
return;
|
|
8670
8762
|
}
|
|
8671
8763
|
const errorDetail = response.error ?? `HTTP ${response.status}`;
|
|
8672
|
-
|
|
8764
|
+
log15.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
|
|
8673
8765
|
if (attempt === maxRetries - 1) {
|
|
8674
8766
|
this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
|
|
8675
8767
|
}
|
|
@@ -8677,7 +8769,7 @@ var Notifier = class {
|
|
|
8677
8769
|
const errorDetail = err instanceof Error ? err.message : String(err);
|
|
8678
8770
|
if (attempt === maxRetries - 1) {
|
|
8679
8771
|
this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
|
|
8680
|
-
|
|
8772
|
+
log15.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
|
|
8681
8773
|
}
|
|
8682
8774
|
}
|
|
8683
8775
|
if (attempt < maxRetries - 1) {
|
|
@@ -8687,7 +8779,7 @@ var Notifier = class {
|
|
|
8687
8779
|
}
|
|
8688
8780
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
8689
8781
|
this.db.insert(auditLog).values({
|
|
8690
|
-
id:
|
|
8782
|
+
id: crypto17.randomUUID(),
|
|
8691
8783
|
projectId,
|
|
8692
8784
|
actor: "scheduler",
|
|
8693
8785
|
action: `notification.${status}`,
|
|
@@ -8700,8 +8792,8 @@ var Notifier = class {
|
|
|
8700
8792
|
};
|
|
8701
8793
|
|
|
8702
8794
|
// src/run-coordinator.ts
|
|
8703
|
-
import { eq as
|
|
8704
|
-
var
|
|
8795
|
+
import { eq as eq16 } from "drizzle-orm";
|
|
8796
|
+
var log16 = createLogger("RunCoordinator");
|
|
8705
8797
|
var RunCoordinator = class {
|
|
8706
8798
|
constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
8707
8799
|
this.db = db;
|
|
@@ -8716,10 +8808,10 @@ var RunCoordinator = class {
|
|
|
8716
8808
|
onInsightsGenerated;
|
|
8717
8809
|
onAeroEvent;
|
|
8718
8810
|
async onRunCompleted(runId, projectId) {
|
|
8719
|
-
const runRow = this.db.select().from(runs).where(
|
|
8811
|
+
const runRow = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
8720
8812
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
8721
8813
|
if (runRow?.trigger === RunTriggers.probe) {
|
|
8722
|
-
|
|
8814
|
+
log16.info("probe.skip-side-effects", { runId, projectId, kind });
|
|
8723
8815
|
return;
|
|
8724
8816
|
}
|
|
8725
8817
|
let insightCount = 0;
|
|
@@ -8736,12 +8828,12 @@ var RunCoordinator = class {
|
|
|
8736
8828
|
try {
|
|
8737
8829
|
await this.onInsightsGenerated(runId, projectId, result);
|
|
8738
8830
|
} catch (err) {
|
|
8739
|
-
|
|
8831
|
+
log16.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8740
8832
|
}
|
|
8741
8833
|
}
|
|
8742
8834
|
}
|
|
8743
8835
|
} catch (err) {
|
|
8744
|
-
|
|
8836
|
+
log16.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8745
8837
|
}
|
|
8746
8838
|
} else if (kind === RunKinds["gbp-sync"]) {
|
|
8747
8839
|
try {
|
|
@@ -8754,17 +8846,17 @@ var RunCoordinator = class {
|
|
|
8754
8846
|
try {
|
|
8755
8847
|
await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
|
|
8756
8848
|
} catch (err) {
|
|
8757
|
-
|
|
8849
|
+
log16.error("gbp-insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8758
8850
|
}
|
|
8759
8851
|
}
|
|
8760
8852
|
} catch (err) {
|
|
8761
|
-
|
|
8853
|
+
log16.error("gbp-intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8762
8854
|
}
|
|
8763
8855
|
}
|
|
8764
8856
|
try {
|
|
8765
8857
|
await this.notifier.onRunCompleted(runId, projectId);
|
|
8766
8858
|
} catch (err) {
|
|
8767
|
-
|
|
8859
|
+
log16.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8768
8860
|
}
|
|
8769
8861
|
if (this.onAeroEvent) {
|
|
8770
8862
|
try {
|
|
@@ -8777,7 +8869,7 @@ var RunCoordinator = class {
|
|
|
8777
8869
|
};
|
|
8778
8870
|
await this.onAeroEvent(ctx);
|
|
8779
8871
|
} catch (err) {
|
|
8780
|
-
|
|
8872
|
+
log16.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8781
8873
|
}
|
|
8782
8874
|
}
|
|
8783
8875
|
}
|
|
@@ -8792,7 +8884,7 @@ var RunCoordinator = class {
|
|
|
8792
8884
|
* so the Aero queue is never starved of a follow-up.
|
|
8793
8885
|
*/
|
|
8794
8886
|
buildDiscoveryAeroContext(runId, projectId, status, error) {
|
|
8795
|
-
const session = this.db.select().from(discoverySessions).where(
|
|
8887
|
+
const session = this.db.select().from(discoverySessions).where(eq16(discoverySessions.runId, runId)).get();
|
|
8796
8888
|
const competitorMap = session ? session.competitorMap : [];
|
|
8797
8889
|
return {
|
|
8798
8890
|
kind: RunKinds["aeo-discover-probe"],
|
|
@@ -8834,8 +8926,8 @@ function analysisResultFromInsights(insights2) {
|
|
|
8834
8926
|
}
|
|
8835
8927
|
|
|
8836
8928
|
// src/agent/session-registry.ts
|
|
8837
|
-
import
|
|
8838
|
-
import { eq as
|
|
8929
|
+
import crypto21 from "crypto";
|
|
8930
|
+
import { eq as eq18 } from "drizzle-orm";
|
|
8839
8931
|
|
|
8840
8932
|
// src/agent/session.ts
|
|
8841
8933
|
import fs7 from "fs";
|
|
@@ -9578,7 +9670,7 @@ function buildAeroStateTools(ctx, opts = {}) {
|
|
|
9578
9670
|
}
|
|
9579
9671
|
|
|
9580
9672
|
// src/agent/llm-usage.ts
|
|
9581
|
-
import
|
|
9673
|
+
import crypto18 from "crypto";
|
|
9582
9674
|
var AeroLlmUsageFeatures = {
|
|
9583
9675
|
turn: "aero.turn"
|
|
9584
9676
|
};
|
|
@@ -9597,7 +9689,7 @@ function recordLlmUsageEvent(args) {
|
|
|
9597
9689
|
const usage = args.message.usage;
|
|
9598
9690
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9599
9691
|
args.db.insert(llmUsageEvents).values({
|
|
9600
|
-
id:
|
|
9692
|
+
id: crypto18.randomUUID(),
|
|
9601
9693
|
projectId: args.projectId,
|
|
9602
9694
|
runId: args.runId,
|
|
9603
9695
|
agentSessionId: args.agentSessionId,
|
|
@@ -9673,7 +9765,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
|
|
|
9673
9765
|
}
|
|
9674
9766
|
|
|
9675
9767
|
// src/agent/tool-usage.ts
|
|
9676
|
-
import
|
|
9768
|
+
import crypto19 from "crypto";
|
|
9677
9769
|
var AeroToolEventStatuses = {
|
|
9678
9770
|
success: "success",
|
|
9679
9771
|
error: "error"
|
|
@@ -9736,7 +9828,7 @@ function createAeroToolUsageHooks(args) {
|
|
|
9736
9828
|
function recordAgentToolEvent(args) {
|
|
9737
9829
|
try {
|
|
9738
9830
|
args.db.insert(agentToolEvents).values({
|
|
9739
|
-
id:
|
|
9831
|
+
id: crypto19.randomUUID(),
|
|
9740
9832
|
projectId: args.projectId,
|
|
9741
9833
|
agentSessionId: args.agentSessionId,
|
|
9742
9834
|
toolCallId: args.toolCallId,
|
|
@@ -9879,7 +9971,7 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
9879
9971
|
import { Type as Type4 } from "@sinclair/typebox";
|
|
9880
9972
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9881
9973
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9882
|
-
var
|
|
9974
|
+
var log17 = createLogger("RemoteMcp");
|
|
9883
9975
|
var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
|
|
9884
9976
|
var MAX_TOOL_RESULT_CHARS2 = 2e4;
|
|
9885
9977
|
function truncate(text) {
|
|
@@ -9932,7 +10024,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9932
10024
|
try {
|
|
9933
10025
|
client = await connect(server);
|
|
9934
10026
|
} catch (err) {
|
|
9935
|
-
|
|
10027
|
+
log17.error("external-mcp.connect-failed", {
|
|
9936
10028
|
label,
|
|
9937
10029
|
error: err instanceof Error ? err.message : String(err)
|
|
9938
10030
|
});
|
|
@@ -9942,7 +10034,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9942
10034
|
try {
|
|
9943
10035
|
listed = await client.listTools();
|
|
9944
10036
|
} catch (err) {
|
|
9945
|
-
|
|
10037
|
+
log17.error("external-mcp.list-failed", {
|
|
9946
10038
|
label,
|
|
9947
10039
|
error: err instanceof Error ? err.message : String(err)
|
|
9948
10040
|
});
|
|
@@ -9953,7 +10045,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9953
10045
|
if (!isReadOnly(tool)) continue;
|
|
9954
10046
|
tools.push(adaptRemoteTool(client, tool));
|
|
9955
10047
|
}
|
|
9956
|
-
|
|
10048
|
+
log17.info("external-mcp.loaded", {
|
|
9957
10049
|
label,
|
|
9958
10050
|
discovered: listed.tools.length,
|
|
9959
10051
|
adopted: tools.length
|
|
@@ -9963,8 +10055,8 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9963
10055
|
}
|
|
9964
10056
|
|
|
9965
10057
|
// src/agent/memory-store.ts
|
|
9966
|
-
import
|
|
9967
|
-
import { and as
|
|
10058
|
+
import crypto20 from "crypto";
|
|
10059
|
+
import { and as and13, desc as desc6, eq as eq17, like, sql as sql7 } from "drizzle-orm";
|
|
9968
10060
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
9969
10061
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
9970
10062
|
function rowToDto(row) {
|
|
@@ -9978,7 +10070,7 @@ function rowToDto(row) {
|
|
|
9978
10070
|
};
|
|
9979
10071
|
}
|
|
9980
10072
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
9981
|
-
const query = db.select().from(agentMemory).where(
|
|
10073
|
+
const query = db.select().from(agentMemory).where(eq17(agentMemory.projectId, projectId)).orderBy(desc6(agentMemory.updatedAt));
|
|
9982
10074
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
9983
10075
|
return rows.map(rowToDto);
|
|
9984
10076
|
}
|
|
@@ -9992,7 +10084,7 @@ function upsertMemoryEntry(db, args) {
|
|
|
9992
10084
|
throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
|
|
9993
10085
|
}
|
|
9994
10086
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9995
|
-
const id =
|
|
10087
|
+
const id = crypto20.randomUUID();
|
|
9996
10088
|
db.insert(agentMemory).values({
|
|
9997
10089
|
id,
|
|
9998
10090
|
projectId: args.projectId,
|
|
@@ -10009,12 +10101,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
10009
10101
|
updatedAt: now
|
|
10010
10102
|
}
|
|
10011
10103
|
}).run();
|
|
10012
|
-
const row = db.select().from(agentMemory).where(
|
|
10104
|
+
const row = db.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, args.key))).get();
|
|
10013
10105
|
if (!row) throw new Error("memory upsert produced no row");
|
|
10014
10106
|
return rowToDto(row);
|
|
10015
10107
|
}
|
|
10016
10108
|
function deleteMemoryEntry(db, projectId, key) {
|
|
10017
|
-
const result = db.delete(agentMemory).where(
|
|
10109
|
+
const result = db.delete(agentMemory).where(and13(eq17(agentMemory.projectId, projectId), eq17(agentMemory.key, key))).run();
|
|
10018
10110
|
const changes = result.changes ?? 0;
|
|
10019
10111
|
return changes > 0;
|
|
10020
10112
|
}
|
|
@@ -10029,7 +10121,7 @@ function writeCompactionNote(db, args) {
|
|
|
10029
10121
|
}
|
|
10030
10122
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10031
10123
|
const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
|
|
10032
|
-
const id =
|
|
10124
|
+
const id = crypto20.randomUUID();
|
|
10033
10125
|
let inserted;
|
|
10034
10126
|
db.transaction((tx) => {
|
|
10035
10127
|
tx.insert(agentMemory).values({
|
|
@@ -10043,16 +10135,16 @@ function writeCompactionNote(db, args) {
|
|
|
10043
10135
|
}).run();
|
|
10044
10136
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
10045
10137
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
10046
|
-
|
|
10047
|
-
|
|
10138
|
+
and13(
|
|
10139
|
+
eq17(agentMemory.projectId, args.projectId),
|
|
10048
10140
|
like(agentMemory.key, `${sessionPrefix}%`)
|
|
10049
10141
|
)
|
|
10050
10142
|
).orderBy(desc6(agentMemory.updatedAt)).all();
|
|
10051
10143
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
10052
10144
|
if (stale.length > 0) {
|
|
10053
|
-
tx.delete(agentMemory).where(
|
|
10145
|
+
tx.delete(agentMemory).where(sql7`${agentMemory.id} IN (${sql7.join(stale.map((s) => sql7`${s}`), sql7`, `)})`).run();
|
|
10054
10146
|
}
|
|
10055
|
-
const row = tx.select().from(agentMemory).where(
|
|
10147
|
+
const row = tx.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, key))).get();
|
|
10056
10148
|
if (row) inserted = rowToDto(row);
|
|
10057
10149
|
});
|
|
10058
10150
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -10185,7 +10277,7 @@ async function compactMessages(args) {
|
|
|
10185
10277
|
}
|
|
10186
10278
|
|
|
10187
10279
|
// src/agent/session-registry.ts
|
|
10188
|
-
var
|
|
10280
|
+
var log18 = createLogger("SessionRegistry");
|
|
10189
10281
|
var MAX_HYDRATE_NOTES = 20;
|
|
10190
10282
|
var MAX_HYDRATE_BYTES = 32 * 1024;
|
|
10191
10283
|
function escapeMemoryFragment(value) {
|
|
@@ -10229,7 +10321,7 @@ var SessionRegistry = class {
|
|
|
10229
10321
|
if (!this.externalToolsPromise) {
|
|
10230
10322
|
this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
|
|
10231
10323
|
(err) => {
|
|
10232
|
-
|
|
10324
|
+
log18.error("external-mcp.load-failed", {
|
|
10233
10325
|
error: err instanceof Error ? err.message : String(err)
|
|
10234
10326
|
});
|
|
10235
10327
|
return [];
|
|
@@ -10279,7 +10371,7 @@ var SessionRegistry = class {
|
|
|
10279
10371
|
modelProvider: effectiveProvider,
|
|
10280
10372
|
modelId: effectiveModelId,
|
|
10281
10373
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10282
|
-
}).where(
|
|
10374
|
+
}).where(eq18(agentSessions.projectId, projectId)).run();
|
|
10283
10375
|
}
|
|
10284
10376
|
const agent2 = createAeroSession({
|
|
10285
10377
|
projectName,
|
|
@@ -10308,7 +10400,7 @@ var SessionRegistry = class {
|
|
|
10308
10400
|
}
|
|
10309
10401
|
const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
|
|
10310
10402
|
const systemPrompt = loadAeroSystemPrompt();
|
|
10311
|
-
const sessionId =
|
|
10403
|
+
const sessionId = crypto21.randomUUID();
|
|
10312
10404
|
const agent = createAeroSession({
|
|
10313
10405
|
projectName,
|
|
10314
10406
|
client: this.opts.client,
|
|
@@ -10474,13 +10566,13 @@ ${lines.join("\n")}
|
|
|
10474
10566
|
agent.state.messages = result.messages;
|
|
10475
10567
|
agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
|
|
10476
10568
|
this.save(projectName);
|
|
10477
|
-
|
|
10569
|
+
log18.info("compaction.completed", {
|
|
10478
10570
|
projectName,
|
|
10479
10571
|
removedCount: result.removedCount,
|
|
10480
10572
|
summaryBytes: Buffer.byteLength(result.summary, "utf8")
|
|
10481
10573
|
});
|
|
10482
10574
|
} catch (err) {
|
|
10483
|
-
|
|
10575
|
+
log18.error("compaction.failed", {
|
|
10484
10576
|
projectName,
|
|
10485
10577
|
error: err instanceof Error ? err.message : String(err)
|
|
10486
10578
|
});
|
|
@@ -10513,7 +10605,7 @@ ${lines.join("\n")}
|
|
|
10513
10605
|
modelProvider: nextProvider,
|
|
10514
10606
|
modelId: nextModelId,
|
|
10515
10607
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10516
|
-
}).where(
|
|
10608
|
+
}).where(eq18(agentSessions.projectId, projectId)).run();
|
|
10517
10609
|
}
|
|
10518
10610
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
10519
10611
|
save(projectName) {
|
|
@@ -10581,7 +10673,7 @@ ${lines.join("\n")}
|
|
|
10581
10673
|
await agent.prompt(msgs);
|
|
10582
10674
|
this.save(projectName);
|
|
10583
10675
|
} catch (err) {
|
|
10584
|
-
|
|
10676
|
+
log18.error("drain.failed", {
|
|
10585
10677
|
projectName,
|
|
10586
10678
|
error: err instanceof Error ? err.message : String(err)
|
|
10587
10679
|
});
|
|
@@ -10677,17 +10769,17 @@ ${lines.join("\n")}
|
|
|
10677
10769
|
return id;
|
|
10678
10770
|
}
|
|
10679
10771
|
tryResolveProjectId(projectName) {
|
|
10680
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
10772
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq18(projects.name, projectName)).get();
|
|
10681
10773
|
return row?.id;
|
|
10682
10774
|
}
|
|
10683
10775
|
loadRow(projectId) {
|
|
10684
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
10776
|
+
const row = this.opts.db.select().from(agentSessions).where(eq18(agentSessions.projectId, projectId)).get();
|
|
10685
10777
|
return row ?? null;
|
|
10686
10778
|
}
|
|
10687
10779
|
insertRow(params) {
|
|
10688
10780
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10689
10781
|
this.opts.db.insert(agentSessions).values({
|
|
10690
|
-
id: params.id ??
|
|
10782
|
+
id: params.id ?? crypto21.randomUUID(),
|
|
10691
10783
|
projectId: params.projectId,
|
|
10692
10784
|
systemPrompt: params.systemPrompt,
|
|
10693
10785
|
modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
|
|
@@ -10700,14 +10792,14 @@ ${lines.join("\n")}
|
|
|
10700
10792
|
}
|
|
10701
10793
|
updateRow(projectId, patch) {
|
|
10702
10794
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10703
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
10795
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq18(agentSessions.projectId, projectId)).run();
|
|
10704
10796
|
}
|
|
10705
10797
|
};
|
|
10706
10798
|
|
|
10707
10799
|
// src/agent/agent-routes.ts
|
|
10708
|
-
import { eq as
|
|
10800
|
+
import { eq as eq19 } from "drizzle-orm";
|
|
10709
10801
|
function resolveProject(db, name) {
|
|
10710
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
10802
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq19(projects.name, name)).get();
|
|
10711
10803
|
if (!row) throw notFound("project", name);
|
|
10712
10804
|
return row;
|
|
10713
10805
|
}
|
|
@@ -10716,7 +10808,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
10716
10808
|
"/projects/:name/agent/transcript",
|
|
10717
10809
|
async (request) => {
|
|
10718
10810
|
const project = resolveProject(opts.db, request.params.name);
|
|
10719
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
10811
|
+
const row = opts.db.select().from(agentSessions).where(eq19(agentSessions.projectId, project.id)).get();
|
|
10720
10812
|
if (!row) {
|
|
10721
10813
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
10722
10814
|
}
|
|
@@ -10740,7 +10832,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
10740
10832
|
async (request) => {
|
|
10741
10833
|
const project = resolveProject(opts.db, request.params.name);
|
|
10742
10834
|
opts.sessionRegistry.reset(project.name);
|
|
10743
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
10835
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(agentSessions.projectId, project.id)).run();
|
|
10744
10836
|
return { status: "reset" };
|
|
10745
10837
|
}
|
|
10746
10838
|
);
|
|
@@ -11174,7 +11266,7 @@ function formatAuditFactorScore(factor) {
|
|
|
11174
11266
|
}
|
|
11175
11267
|
|
|
11176
11268
|
// src/snapshot-service.ts
|
|
11177
|
-
var
|
|
11269
|
+
var log19 = createLogger("Snapshot");
|
|
11178
11270
|
var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
|
|
11179
11271
|
var SNAPSHOT_QUERY_COUNT = 6;
|
|
11180
11272
|
var ProviderExecutionGate2 = class {
|
|
@@ -11320,7 +11412,7 @@ var SnapshotService = class {
|
|
|
11320
11412
|
return mapAuditReport(report);
|
|
11321
11413
|
} catch (err) {
|
|
11322
11414
|
const message = err instanceof Error ? err.message : String(err);
|
|
11323
|
-
|
|
11415
|
+
log19.warn("audit.failed", { homepageUrl, error: message });
|
|
11324
11416
|
return {
|
|
11325
11417
|
url: homepageUrl,
|
|
11326
11418
|
finalUrl: homepageUrl,
|
|
@@ -11349,7 +11441,7 @@ var SnapshotService = class {
|
|
|
11349
11441
|
queries: parsedQueries
|
|
11350
11442
|
};
|
|
11351
11443
|
} catch (err) {
|
|
11352
|
-
|
|
11444
|
+
log19.warn("profile.generation-failed", {
|
|
11353
11445
|
domain: ctx.domain,
|
|
11354
11446
|
provider: ctx.analysisProvider.adapter.name,
|
|
11355
11447
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -11491,7 +11583,7 @@ var SnapshotService = class {
|
|
|
11491
11583
|
recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
|
|
11492
11584
|
};
|
|
11493
11585
|
} catch (err) {
|
|
11494
|
-
|
|
11586
|
+
log19.warn("response.analysis-failed", {
|
|
11495
11587
|
provider: ctx.analysisProvider.adapter.name,
|
|
11496
11588
|
error: err instanceof Error ? err.message : String(err)
|
|
11497
11589
|
});
|
|
@@ -11746,20 +11838,20 @@ function clipText(value, length) {
|
|
|
11746
11838
|
}
|
|
11747
11839
|
|
|
11748
11840
|
// src/research-runner.ts
|
|
11749
|
-
import { and as
|
|
11841
|
+
import { and as and14, eq as eq20, inArray as inArray7, sql as sql8 } from "drizzle-orm";
|
|
11750
11842
|
var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
|
|
11751
11843
|
function finalResearchRunStatus(completed, failed) {
|
|
11752
11844
|
if (failed === 0) return ResearchRunStatuses.completed;
|
|
11753
11845
|
return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
|
|
11754
11846
|
}
|
|
11755
11847
|
async function executeResearchRun(db, registry, runId, projectId) {
|
|
11756
|
-
const run = db.select().from(researchRuns).where(
|
|
11848
|
+
const run = db.select().from(researchRuns).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.projectId, projectId))).get();
|
|
11757
11849
|
if (!run || run.status !== ResearchRunStatuses.queued) return;
|
|
11758
|
-
const project = db.select().from(projects).where(
|
|
11850
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
11759
11851
|
if (!project) return;
|
|
11760
11852
|
const provider = registry.get(run.provider);
|
|
11761
11853
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11762
|
-
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(
|
|
11854
|
+
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.status, ResearchRunStatuses.queued))).run();
|
|
11763
11855
|
if (claim.changes !== 1) return;
|
|
11764
11856
|
let reserved = 0;
|
|
11765
11857
|
let dispatched = 0;
|
|
@@ -11783,8 +11875,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11783
11875
|
}
|
|
11784
11876
|
reserved = run.totalQueries;
|
|
11785
11877
|
reservation = { scope, period };
|
|
11786
|
-
const rows = db.select().from(researchRunQueries).where(
|
|
11787
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
11878
|
+
const rows = db.select().from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
|
|
11879
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq20(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
11788
11880
|
const domains = effectiveDomains(project);
|
|
11789
11881
|
const brands = effectiveBrandNames(project);
|
|
11790
11882
|
const config = { ...provider.config, model: run.resolvedModel };
|
|
@@ -11792,7 +11884,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11792
11884
|
await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
|
|
11793
11885
|
try {
|
|
11794
11886
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11795
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(
|
|
11887
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
|
|
11796
11888
|
const raw = await gate.run(async () => {
|
|
11797
11889
|
dispatched++;
|
|
11798
11890
|
return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
|
|
@@ -11819,7 +11911,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11819
11911
|
citationState: determineCitationState(normalized, domains),
|
|
11820
11912
|
rawResponse: raw.rawResponse,
|
|
11821
11913
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11822
|
-
}).where(
|
|
11914
|
+
}).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.running))).run();
|
|
11823
11915
|
if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
|
|
11824
11916
|
} catch (error) {
|
|
11825
11917
|
try {
|
|
@@ -11843,18 +11935,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11843
11935
|
}
|
|
11844
11936
|
}
|
|
11845
11937
|
function incrementResearchProgress(db, runId, column) {
|
|
11846
|
-
db.update(researchRuns).set({ [column]:
|
|
11938
|
+
db.update(researchRuns).set({ [column]: sql8`${researchRuns[column]} + 1` }).where(eq20(researchRuns.id, runId)).run();
|
|
11847
11939
|
}
|
|
11848
11940
|
function markResearchQueryFailed(db, runId, queryId, error) {
|
|
11849
11941
|
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(
|
|
11942
|
+
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
11943
|
if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
|
|
11852
11944
|
}
|
|
11853
11945
|
function markUnfinishedResearchQueriesFailed(db, runId, error) {
|
|
11854
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
11946
|
+
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
11947
|
}
|
|
11856
11948
|
function finalizeResearchRun(db, runId, fatalError) {
|
|
11857
|
-
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(
|
|
11949
|
+
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).all();
|
|
11858
11950
|
const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
|
|
11859
11951
|
const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
|
|
11860
11952
|
db.update(researchRuns).set({
|
|
@@ -11863,13 +11955,13 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
11863
11955
|
failedQueries: failed,
|
|
11864
11956
|
error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
|
|
11865
11957
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11866
|
-
}).where(
|
|
11958
|
+
}).where(eq20(researchRuns.id, runId)).run();
|
|
11867
11959
|
}
|
|
11868
11960
|
|
|
11869
11961
|
// src/server.ts
|
|
11870
11962
|
var _require3 = createRequire3(import.meta.url);
|
|
11871
11963
|
var { version: PKG_VERSION2 } = _require3("../package.json");
|
|
11872
|
-
var
|
|
11964
|
+
var log20 = createLogger("Server");
|
|
11873
11965
|
var DEFAULT_QUOTA = {
|
|
11874
11966
|
maxConcurrency: 2,
|
|
11875
11967
|
maxRequestsPerMinute: 10,
|
|
@@ -11912,14 +12004,14 @@ function summarizeProviderConfig(config) {
|
|
|
11912
12004
|
};
|
|
11913
12005
|
}
|
|
11914
12006
|
function hashApiKey(key) {
|
|
11915
|
-
return
|
|
12007
|
+
return crypto22.createHash("sha256").update(key).digest("hex");
|
|
11916
12008
|
}
|
|
11917
12009
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
11918
12010
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
11919
12011
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
11920
12012
|
function hashDashboardPassword(password) {
|
|
11921
|
-
const salt =
|
|
11922
|
-
const derived =
|
|
12013
|
+
const salt = crypto22.randomBytes(16);
|
|
12014
|
+
const derived = crypto22.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
11923
12015
|
N: DASHBOARD_SCRYPT_COST,
|
|
11924
12016
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
11925
12017
|
});
|
|
@@ -11940,14 +12032,14 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
11940
12032
|
} catch {
|
|
11941
12033
|
return { ok: false, needsRehash: false };
|
|
11942
12034
|
}
|
|
11943
|
-
const derived =
|
|
12035
|
+
const derived = crypto22.scryptSync(password, salt, expected.length, {
|
|
11944
12036
|
N: DASHBOARD_SCRYPT_COST,
|
|
11945
12037
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
11946
12038
|
});
|
|
11947
12039
|
if (derived.length !== expected.length)
|
|
11948
12040
|
return { ok: false, needsRehash: false };
|
|
11949
12041
|
return {
|
|
11950
|
-
ok:
|
|
12042
|
+
ok: crypto22.timingSafeEqual(derived, expected),
|
|
11951
12043
|
needsRehash: false
|
|
11952
12044
|
};
|
|
11953
12045
|
}
|
|
@@ -11956,7 +12048,7 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
11956
12048
|
const expected = Buffer.from(storedHash, "hex");
|
|
11957
12049
|
if (candidate.length !== expected.length)
|
|
11958
12050
|
return { ok: false, needsRehash: false };
|
|
11959
|
-
const ok =
|
|
12051
|
+
const ok = crypto22.timingSafeEqual(candidate, expected);
|
|
11960
12052
|
return { ok, needsRehash: ok };
|
|
11961
12053
|
}
|
|
11962
12054
|
return { ok: false, needsRehash: false };
|
|
@@ -12019,7 +12111,7 @@ function applyLegacyCredentials(rows, config) {
|
|
|
12019
12111
|
}
|
|
12020
12112
|
if (migratedGoogle > 0) {
|
|
12021
12113
|
saveConfigPatch({ google: config.google });
|
|
12022
|
-
|
|
12114
|
+
log20.info("credentials.migrated", { type: "google", count: migratedGoogle });
|
|
12023
12115
|
}
|
|
12024
12116
|
let migratedGa4 = 0;
|
|
12025
12117
|
for (const row of rows.ga4) {
|
|
@@ -12037,7 +12129,7 @@ function applyLegacyCredentials(rows, config) {
|
|
|
12037
12129
|
}
|
|
12038
12130
|
if (migratedGa4 > 0) {
|
|
12039
12131
|
saveConfigPatch({ ga4: config.ga4 });
|
|
12040
|
-
|
|
12132
|
+
log20.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
|
|
12041
12133
|
}
|
|
12042
12134
|
}
|
|
12043
12135
|
function isLoopbackBindHost(host) {
|
|
@@ -12094,11 +12186,11 @@ async function createServer(opts) {
|
|
|
12094
12186
|
applyLegacyCredentials(legacyRows, opts.config);
|
|
12095
12187
|
dropLegacyCredentialColumns(opts.db);
|
|
12096
12188
|
} catch (err) {
|
|
12097
|
-
|
|
12189
|
+
log20.warn("credentials.migration.failed", {
|
|
12098
12190
|
error: err instanceof Error ? err.message : String(err)
|
|
12099
12191
|
});
|
|
12100
12192
|
}
|
|
12101
|
-
|
|
12193
|
+
log20.info("providers.configured", {
|
|
12102
12194
|
providers: Object.keys(providers).filter((k) => {
|
|
12103
12195
|
const p = providers[k];
|
|
12104
12196
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
@@ -12162,7 +12254,7 @@ async function createServer(opts) {
|
|
|
12162
12254
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
12163
12255
|
async (ctx) => {
|
|
12164
12256
|
if (!sessionRegistry) return;
|
|
12165
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
12257
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq21(projects.id, ctx.projectId)).get();
|
|
12166
12258
|
if (!project) return;
|
|
12167
12259
|
let content;
|
|
12168
12260
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -12497,6 +12589,24 @@ async function createServer(opts) {
|
|
|
12497
12589
|
onAdsSyncRequested: (runId, projectId) => {
|
|
12498
12590
|
runAdsSync(runId, projectId);
|
|
12499
12591
|
},
|
|
12592
|
+
onDoctorRequested: (projectName) => {
|
|
12593
|
+
void (async () => {
|
|
12594
|
+
try {
|
|
12595
|
+
const report = await aeroClient.runDoctor({ project: projectName });
|
|
12596
|
+
const project = opts.db.select().from(projects).where(eq21(projects.name, projectName)).get();
|
|
12597
|
+
if (!project) {
|
|
12598
|
+
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
12599
|
+
return;
|
|
12600
|
+
}
|
|
12601
|
+
await notifier.onHealthChecked(project.id, {
|
|
12602
|
+
checks: report.checks,
|
|
12603
|
+
checkedAt: report.generatedAt
|
|
12604
|
+
});
|
|
12605
|
+
} catch (err) {
|
|
12606
|
+
app.log.warn({ projectName, err }, "scheduled doctor pass failed");
|
|
12607
|
+
}
|
|
12608
|
+
})();
|
|
12609
|
+
},
|
|
12500
12610
|
onDataRefreshRequested: (projectName) => {
|
|
12501
12611
|
void refreshAllIntegrations(aeroClient, projectName);
|
|
12502
12612
|
},
|
|
@@ -12511,9 +12621,9 @@ async function createServer(opts) {
|
|
|
12511
12621
|
});
|
|
12512
12622
|
if (!probed) return;
|
|
12513
12623
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12624
|
+
and15(
|
|
12625
|
+
eq21(ccReleaseSyncs.release, probed.release),
|
|
12626
|
+
eq21(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
12517
12627
|
)
|
|
12518
12628
|
).limit(1).get();
|
|
12519
12629
|
if (alreadySynced) {
|
|
@@ -12534,18 +12644,6 @@ async function createServer(opts) {
|
|
|
12534
12644
|
);
|
|
12535
12645
|
});
|
|
12536
12646
|
})();
|
|
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
12647
|
},
|
|
12550
12648
|
onSiteAuditRequested: (runId, projectId) => {
|
|
12551
12649
|
runSiteAudit(runId, projectId);
|
|
@@ -12680,7 +12778,7 @@ async function createServer(opts) {
|
|
|
12680
12778
|
return removed;
|
|
12681
12779
|
}
|
|
12682
12780
|
};
|
|
12683
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
12781
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
|
|
12684
12782
|
const googleConnectionStore = {
|
|
12685
12783
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
12686
12784
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -12753,11 +12851,11 @@ async function createServer(opts) {
|
|
|
12753
12851
|
const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
|
|
12754
12852
|
if (opts.config.apiKey) {
|
|
12755
12853
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
12756
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
12854
|
+
const existing = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, keyHash)).get();
|
|
12757
12855
|
if (!existing) {
|
|
12758
12856
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
12759
12857
|
opts.db.insert(apiKeys).values({
|
|
12760
|
-
id: `key_${
|
|
12858
|
+
id: `key_${crypto22.randomBytes(8).toString("hex")}`,
|
|
12761
12859
|
name: "default",
|
|
12762
12860
|
keyHash,
|
|
12763
12861
|
keyPrefix: prefix,
|
|
@@ -12781,7 +12879,7 @@ async function createServer(opts) {
|
|
|
12781
12879
|
};
|
|
12782
12880
|
const createSession = (apiKeyId) => {
|
|
12783
12881
|
pruneExpiredSessions();
|
|
12784
|
-
const sessionId =
|
|
12882
|
+
const sessionId = crypto22.randomBytes(32).toString("hex");
|
|
12785
12883
|
sessions.set(sessionId, {
|
|
12786
12884
|
apiKeyId,
|
|
12787
12885
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -12805,7 +12903,7 @@ async function createServer(opts) {
|
|
|
12805
12903
|
};
|
|
12806
12904
|
const getDefaultApiKey = () => {
|
|
12807
12905
|
if (!opts.config.apiKey) return void 0;
|
|
12808
|
-
return opts.db.select().from(apiKeys).where(
|
|
12906
|
+
return opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
12809
12907
|
};
|
|
12810
12908
|
const createPasswordSession = (reply) => {
|
|
12811
12909
|
const key = getDefaultApiKey();
|
|
@@ -12829,7 +12927,7 @@ async function createServer(opts) {
|
|
|
12829
12927
|
if (!header) return false;
|
|
12830
12928
|
const parts = header.split(" ");
|
|
12831
12929
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
12832
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
12930
|
+
const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(parts[1]))).get();
|
|
12833
12931
|
return Boolean(key && !key.revokedAt);
|
|
12834
12932
|
};
|
|
12835
12933
|
app.get(apiPrefix + "/session", async (request, reply) => {
|
|
@@ -12906,12 +13004,12 @@ async function createServer(opts) {
|
|
|
12906
13004
|
return reply.send({ authenticated: true });
|
|
12907
13005
|
}
|
|
12908
13006
|
if (apiKey) {
|
|
12909
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
13007
|
+
const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
12910
13008
|
if (!key || key.revokedAt) {
|
|
12911
13009
|
const err2 = authInvalid();
|
|
12912
13010
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
12913
13011
|
}
|
|
12914
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13012
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(apiKeys.id, key.id)).run();
|
|
12915
13013
|
const sessionId = createSession(key.id);
|
|
12916
13014
|
reply.header(
|
|
12917
13015
|
"set-cookie",
|
|
@@ -13022,6 +13120,11 @@ async function createServer(opts) {
|
|
|
13022
13120
|
// installer; cloud deployments inherit the secure default of `false` by
|
|
13023
13121
|
// not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
|
|
13024
13122
|
allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
|
|
13123
|
+
// Wall-clock budget for one incremental Vercel drain. This is the only lever
|
|
13124
|
+
// that decides whether a source catches up or falls further behind, and it
|
|
13125
|
+
// was previously reachable only from tests, so a source losing ground could
|
|
13126
|
+
// not be rescued without shipping code. Unset keeps the built-in default.
|
|
13127
|
+
vercelSyncDeadlineMs: resolveVercelSyncDeadlineMs(process.env),
|
|
13025
13128
|
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
13026
13129
|
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
13027
13130
|
registerAuthenticatedRoutes: async (scope) => {
|
|
@@ -13104,7 +13207,7 @@ async function createServer(opts) {
|
|
|
13104
13207
|
deps: {
|
|
13105
13208
|
enqueueAutoExtract: ({ projectId, release: r }) => {
|
|
13106
13209
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13107
|
-
const runId =
|
|
13210
|
+
const runId = crypto22.randomUUID();
|
|
13108
13211
|
opts.db.insert(runs).values({
|
|
13109
13212
|
id: runId,
|
|
13110
13213
|
projectId,
|
|
@@ -13134,13 +13237,6 @@ async function createServer(opts) {
|
|
|
13134
13237
|
}
|
|
13135
13238
|
);
|
|
13136
13239
|
},
|
|
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
13240
|
onDiscoveryRunRequested: (input) => {
|
|
13145
13241
|
executeDiscoveryRun({
|
|
13146
13242
|
db: opts.db,
|
|
@@ -13236,7 +13332,7 @@ async function createServer(opts) {
|
|
|
13236
13332
|
...inspectOpts,
|
|
13237
13333
|
config: opts.config
|
|
13238
13334
|
}).then(() => {
|
|
13239
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
13335
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq21(runs.id, runId)).get();
|
|
13240
13336
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
13241
13337
|
return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
|
|
13242
13338
|
}
|
|
@@ -13330,7 +13426,7 @@ async function createServer(opts) {
|
|
|
13330
13426
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13331
13427
|
opts.db.insert(auditLog).values(
|
|
13332
13428
|
targetProjectIds.map((projectId) => ({
|
|
13333
|
-
id:
|
|
13429
|
+
id: crypto22.randomUUID(),
|
|
13334
13430
|
projectId,
|
|
13335
13431
|
actor: "api",
|
|
13336
13432
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -13585,7 +13681,7 @@ async function createServer(opts) {
|
|
|
13585
13681
|
}
|
|
13586
13682
|
checkLatestVersionForServer();
|
|
13587
13683
|
scheduler.start();
|
|
13588
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
13684
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq21(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
13589
13685
|
dispatchResearchRun(run.id, run.projectId);
|
|
13590
13686
|
}
|
|
13591
13687
|
app.addHook("onClose", async () => {
|