@canonry/canonry 5.19.0 → 5.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/SKILL.md +13 -8
- package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +27 -10
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -2
- package/assets/assets/{AuditHistoryPanel-C_p8jX7Q.js → AuditHistoryPanel-BSh3Oezj.js} +1 -1
- package/assets/assets/{BacklinksPage-jD4nOGw1.js → BacklinksPage-DQ59dhCx.js} +1 -1
- package/assets/assets/{HistoryPage-DJGBbtBY.js → HistoryPage-BBvgid3o.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Dmr3TVlo.js → MeasurementPropertyPage-Dmb18JbT.js} +1 -1
- package/assets/assets/{ProjectPage-B_haCKiA.js → ProjectPage-CPg0q2Dy.js} +1 -1
- package/assets/assets/{RunRow-BMbwR2yk.js → RunRow-BK8sZCID.js} +1 -1
- package/assets/assets/{RunsPage-D8hHakq2.js → RunsPage-CIcVBTdP.js} +1 -1
- package/assets/assets/{SettingsPage-QKnRcFA9.js → SettingsPage-Bc3aR-gz.js} +1 -1
- package/assets/assets/{SiteHealthSection-CDkUXqmb.js → SiteHealthSection-FF14aIXT.js} +3 -3
- package/assets/assets/{TrafficPage-CMif8RlM.js → TrafficPage-C_qoeGDl.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DOCTP_4-.js → TrafficSourceDetailPage-B3i_B2MI.js} +1 -1
- package/assets/assets/{extract-error-message-Dm8RvYE5.js → extract-error-message-BisUn6nN.js} +1 -1
- package/assets/assets/{index-lj0fuFl4.js → index-Dht7JNX5.js} +44 -44
- package/assets/assets/{react-sigma_core.esm.min-DOPuwLtN.js → react-sigma_core.esm.min-tWhS09SS.js} +1 -1
- package/assets/assets/{v2-overview-adapter-C19tUcDS.js → v2-overview-adapter-CAoeWKQJ.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-QJWIOCEO.js → chunk-4PENJKRN.js} +2 -2
- package/dist/{chunk-SPCEXW3B.js → chunk-5FKR2K76.js} +11 -4
- package/dist/{chunk-GUJY3RIM.js → chunk-DRZWMFRV.js} +627 -263
- package/dist/{chunk-WLL7Q7JD.js → chunk-QF7SY7AF.js} +222 -29
- package/dist/{chunk-ABUO27BV.js → chunk-TSFKNQ5X.js} +3 -3
- package/dist/{chunk-P2EFPLIO.js → chunk-WL27HO5C.js} +565 -139
- package/dist/{chunk-YHKKCXGY.js → chunk-XBGPHZKA.js} +24 -8
- package/dist/cli.js +73 -17
- package/dist/{demo-server-BOIBS74F.js → demo-server-OCBV5OMS.js} +4 -4
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-IWR2H6OW.js → intelligence-service-MKHINDHN.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +14 -14
|
@@ -13,19 +13,19 @@ import {
|
|
|
13
13
|
resolveOperatorApiKeyIds,
|
|
14
14
|
runWithUsageTags,
|
|
15
15
|
saveConfigPatch
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-5FKR2K76.js";
|
|
17
17
|
import {
|
|
18
18
|
deleteSiteCrawlGraphLayout,
|
|
19
19
|
persistSiteCrawlGraphLayout,
|
|
20
20
|
prepareSiteCrawlGraphLayout
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-4PENJKRN.js";
|
|
22
22
|
import {
|
|
23
23
|
CanonryMcpToolNames,
|
|
24
24
|
CliError,
|
|
25
25
|
PACKAGE_VERSION,
|
|
26
26
|
canonryMcpTools,
|
|
27
27
|
isMachineFormat
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-XBGPHZKA.js";
|
|
29
29
|
import {
|
|
30
30
|
CC_CACHE_DIR,
|
|
31
31
|
DUCKDB_SPEC,
|
|
@@ -134,6 +134,8 @@ import {
|
|
|
134
134
|
llmUsageEvents,
|
|
135
135
|
loadDuckdb,
|
|
136
136
|
markSessionFailed,
|
|
137
|
+
measurementPlanVersions,
|
|
138
|
+
measurementPlans,
|
|
137
139
|
measurementRunCompleteness,
|
|
138
140
|
measurementRunSlotState,
|
|
139
141
|
measurementSlotKey,
|
|
@@ -184,7 +186,7 @@ import {
|
|
|
184
186
|
siteLivenessState,
|
|
185
187
|
toAlertView,
|
|
186
188
|
usageCounters
|
|
187
|
-
} from "./chunk-
|
|
189
|
+
} from "./chunk-WL27HO5C.js";
|
|
188
190
|
import {
|
|
189
191
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
190
192
|
AGENT_NONE,
|
|
@@ -213,6 +215,7 @@ import {
|
|
|
213
215
|
GtmSnapshotKinds,
|
|
214
216
|
LLM_CAPABILITIES,
|
|
215
217
|
LlmCapabilities,
|
|
218
|
+
MEASUREMENT_PLAN_V2_SCHEMA_VERSION,
|
|
216
219
|
MemorySources,
|
|
217
220
|
ONBOARDING_FLOW_VERSION,
|
|
218
221
|
ProviderNames,
|
|
@@ -326,6 +329,7 @@ import {
|
|
|
326
329
|
providerError,
|
|
327
330
|
providerSupportsLocationContext,
|
|
328
331
|
registrableDomain,
|
|
332
|
+
resolveProviderModel,
|
|
329
333
|
resolveSnapshotRequestQueries,
|
|
330
334
|
retryAfterDelayMs,
|
|
331
335
|
serializeForInlineScript,
|
|
@@ -347,7 +351,7 @@ import {
|
|
|
347
351
|
visibilityReportRequestSchema,
|
|
348
352
|
winnabilityClassLabel,
|
|
349
353
|
withRetry
|
|
350
|
-
} from "./chunk-
|
|
354
|
+
} from "./chunk-QF7SY7AF.js";
|
|
351
355
|
|
|
352
356
|
// src/runtime-context.ts
|
|
353
357
|
function cliRuntimeContext(env = process.env, stdio = process) {
|
|
@@ -3566,7 +3570,7 @@ import crypto27 from "crypto";
|
|
|
3566
3570
|
import fs8 from "fs";
|
|
3567
3571
|
import path8 from "path";
|
|
3568
3572
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3569
|
-
import { and as
|
|
3573
|
+
import { and as and21, eq as eq27 } from "drizzle-orm";
|
|
3570
3574
|
import Fastify from "fastify";
|
|
3571
3575
|
import os4 from "os";
|
|
3572
3576
|
|
|
@@ -5713,8 +5717,36 @@ async function withRetry6(fn, options = {}) {
|
|
|
5713
5717
|
}
|
|
5714
5718
|
|
|
5715
5719
|
// ../provider-perplexity/src/normalize.ts
|
|
5716
|
-
var DEFAULT_MODEL5 = "
|
|
5717
|
-
var BASE_URL = "https://api.perplexity.ai";
|
|
5720
|
+
var DEFAULT_MODEL5 = "fast";
|
|
5721
|
+
var BASE_URL = "https://api.perplexity.ai/v1";
|
|
5722
|
+
var AGENT_PATH = "/agent";
|
|
5723
|
+
var PERPLEXITY_RETRIEVAL_CONTRACT = "search-required-v1";
|
|
5724
|
+
var RETRIEVAL_OUTPUT_TYPES = /* @__PURE__ */ new Set(["search_results", "fetch_url_results"]);
|
|
5725
|
+
function resolveModel3(model) {
|
|
5726
|
+
const trimmed = model?.trim();
|
|
5727
|
+
return resolveProviderModel("perplexity", trimmed ? trimmed : DEFAULT_MODEL5);
|
|
5728
|
+
}
|
|
5729
|
+
var ANTHROPIC_MAX_OUTPUT_TOKENS = 4096;
|
|
5730
|
+
function agentSelection(model) {
|
|
5731
|
+
if (!model.includes("/")) return { preset: model };
|
|
5732
|
+
return model.startsWith("anthropic/") ? { model, max_output_tokens: ANTHROPIC_MAX_OUTPUT_TOKENS } : { model };
|
|
5733
|
+
}
|
|
5734
|
+
function buildAgentRequest(query, model, location) {
|
|
5735
|
+
const webSearch = { type: "web_search" };
|
|
5736
|
+
if (location) {
|
|
5737
|
+
webSearch.user_location = {
|
|
5738
|
+
city: location.city,
|
|
5739
|
+
region: location.region,
|
|
5740
|
+
country: location.country
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
return {
|
|
5744
|
+
...agentSelection(model),
|
|
5745
|
+
input: query,
|
|
5746
|
+
tools: [webSearch],
|
|
5747
|
+
tool_choice: { type: "web_search" }
|
|
5748
|
+
};
|
|
5749
|
+
}
|
|
5718
5750
|
function validateConfig5(config) {
|
|
5719
5751
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
5720
5752
|
return { ok: false, provider: "perplexity", message: "missing api key" };
|
|
@@ -5723,48 +5755,42 @@ function validateConfig5(config) {
|
|
|
5723
5755
|
ok: true,
|
|
5724
5756
|
provider: "perplexity",
|
|
5725
5757
|
message: "config valid",
|
|
5726
|
-
model: config.model
|
|
5758
|
+
model: resolveModel3(config.model)
|
|
5727
5759
|
};
|
|
5728
5760
|
}
|
|
5729
5761
|
async function healthcheck5(config) {
|
|
5730
5762
|
const validation = validateConfig5(config);
|
|
5731
5763
|
if (!validation.ok) return validation;
|
|
5764
|
+
const model = resolveModel3(config.model);
|
|
5732
5765
|
try {
|
|
5733
|
-
const client =
|
|
5766
|
+
const client = createClient4(config.apiKey);
|
|
5734
5767
|
const response = await withRetry6(
|
|
5735
|
-
() => client
|
|
5736
|
-
model: config.model ?? DEFAULT_MODEL5,
|
|
5737
|
-
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
5738
|
-
})
|
|
5768
|
+
() => postAgent(client, { ...agentSelection(model), input: 'Say "ok"' })
|
|
5739
5769
|
);
|
|
5740
|
-
|
|
5770
|
+
assertUsableResponse(response);
|
|
5771
|
+
const text = extractAgentAnswerText(response);
|
|
5741
5772
|
return {
|
|
5742
5773
|
ok: text.length > 0,
|
|
5743
5774
|
provider: "perplexity",
|
|
5744
5775
|
message: text.length > 0 ? "perplexity api key verified" : "empty response from perplexity",
|
|
5745
|
-
model
|
|
5776
|
+
model
|
|
5746
5777
|
};
|
|
5747
5778
|
} catch (err) {
|
|
5748
5779
|
return {
|
|
5749
5780
|
ok: false,
|
|
5750
5781
|
provider: "perplexity",
|
|
5751
5782
|
message: describeError(err),
|
|
5752
|
-
model
|
|
5783
|
+
model
|
|
5753
5784
|
};
|
|
5754
5785
|
}
|
|
5755
5786
|
}
|
|
5756
5787
|
async function executeTrackedQuery5(input) {
|
|
5757
|
-
const model = input.config.model
|
|
5758
|
-
const client =
|
|
5759
|
-
const
|
|
5788
|
+
const model = resolveModel3(input.config.model);
|
|
5789
|
+
const client = createClient4(input.config.apiKey);
|
|
5790
|
+
const request = buildAgentRequest(input.query, model, input.location);
|
|
5760
5791
|
try {
|
|
5761
|
-
const
|
|
5762
|
-
|
|
5763
|
-
model,
|
|
5764
|
-
messages: [{ role: "user", content: prompt }]
|
|
5765
|
-
})
|
|
5766
|
-
);
|
|
5767
|
-
const rawResponse = responseToRecord5(response);
|
|
5792
|
+
const rawResponse = await withRetry6(() => postAgent(client, request));
|
|
5793
|
+
assertUsableResponse(rawResponse);
|
|
5768
5794
|
const parsed = reparseStoredResult4(rawResponse);
|
|
5769
5795
|
return {
|
|
5770
5796
|
provider: "perplexity",
|
|
@@ -5772,7 +5798,8 @@ async function executeTrackedQuery5(input) {
|
|
|
5772
5798
|
model,
|
|
5773
5799
|
servedModel: extractServedModel5(rawResponse),
|
|
5774
5800
|
groundingSources: parsed.groundingSources,
|
|
5775
|
-
searchQueries: parsed.searchQueries
|
|
5801
|
+
searchQueries: parsed.searchQueries,
|
|
5802
|
+
retrievalStatus: parsed.retrievalStatus
|
|
5776
5803
|
};
|
|
5777
5804
|
} catch (err) {
|
|
5778
5805
|
const msg = describeError(err);
|
|
@@ -5790,10 +5817,13 @@ function normalizeResult6(raw) {
|
|
|
5790
5817
|
answerText: parsed.answerText,
|
|
5791
5818
|
citedDomains,
|
|
5792
5819
|
groundingSources,
|
|
5793
|
-
searchQueries
|
|
5820
|
+
searchQueries,
|
|
5821
|
+
retrievalStatus: useParsed ? parsed.retrievalStatus : raw.retrievalStatus ?? "unknown"
|
|
5794
5822
|
};
|
|
5795
5823
|
}
|
|
5796
5824
|
function hasParsedResponseContent4(rawResponse) {
|
|
5825
|
+
const agent = agentResponseOf(rawResponse);
|
|
5826
|
+
if (agent) return agentOutput(agent).length > 0;
|
|
5797
5827
|
if (Array.isArray(rawResponse.choices) && rawResponse.choices.length > 0) return true;
|
|
5798
5828
|
if (Array.isArray(rawResponse.search_results) && rawResponse.search_results.length > 0) return true;
|
|
5799
5829
|
if (Array.isArray(rawResponse.citations) && rawResponse.citations.length > 0) return true;
|
|
@@ -5805,24 +5835,113 @@ function extractServedModel5(rawResponse) {
|
|
|
5805
5835
|
return normalizeServedModel(rawResponse.model);
|
|
5806
5836
|
}
|
|
5807
5837
|
function reparseStoredResult4(rawResponse) {
|
|
5838
|
+
const agent = agentResponseOf(rawResponse);
|
|
5839
|
+
if (agent) return parseAgentResponse(agent);
|
|
5840
|
+
return parseSonarResponse(rawResponse);
|
|
5841
|
+
}
|
|
5842
|
+
function createClient4(apiKey) {
|
|
5843
|
+
return new OpenAI4({ apiKey, baseURL: BASE_URL });
|
|
5844
|
+
}
|
|
5845
|
+
function postAgent(client, body) {
|
|
5846
|
+
return client.post(AGENT_PATH, { body });
|
|
5847
|
+
}
|
|
5848
|
+
function assertUsableResponse(response) {
|
|
5849
|
+
const status = response.status;
|
|
5850
|
+
if (status === void 0 || status === "completed") return;
|
|
5851
|
+
if (status === "incomplete") {
|
|
5852
|
+
if (extractAgentAnswerText(response).trim().length > 0) return;
|
|
5853
|
+
const details = isRecord(response.incomplete_details) ? response.incomplete_details : void 0;
|
|
5854
|
+
const reason = typeof details?.reason === "string" && details.reason.length > 0 ? details.reason : "no reason given";
|
|
5855
|
+
throw new Error(`agent response incomplete with no answer: ${reason}`);
|
|
5856
|
+
}
|
|
5857
|
+
const error = isRecord(response.error) ? response.error : void 0;
|
|
5858
|
+
const message = typeof error?.message === "string" && error.message.length > 0 ? error.message : "no error detail";
|
|
5859
|
+
const type = typeof error?.type === "string" && error.type.length > 0 ? ` (${error.type})` : "";
|
|
5860
|
+
throw new Error(`agent response ${typeof status === "string" ? status : describeError(status)}: ${message}${type}`);
|
|
5861
|
+
}
|
|
5862
|
+
function agentResponseOf(rawResponse) {
|
|
5863
|
+
if (Array.isArray(rawResponse.output)) return rawResponse;
|
|
5864
|
+
const nested = extractNestedApiResponse(rawResponse);
|
|
5865
|
+
return nested && Array.isArray(nested.output) ? nested : null;
|
|
5866
|
+
}
|
|
5867
|
+
function agentOutput(response) {
|
|
5868
|
+
return Array.isArray(response.output) ? response.output.filter(isRecord) : [];
|
|
5869
|
+
}
|
|
5870
|
+
function parseAgentResponse(response) {
|
|
5871
|
+
const groundingSources = extractAgentGroundingSources(response);
|
|
5872
|
+
return {
|
|
5873
|
+
provider: "perplexity",
|
|
5874
|
+
answerText: extractAgentAnswerText(response),
|
|
5875
|
+
citedDomains: extractCitedDomains2(groundingSources),
|
|
5876
|
+
groundingSources,
|
|
5877
|
+
searchQueries: extractAgentSearchQueries(response),
|
|
5878
|
+
retrievalStatus: extractAgentRetrievalStatus(response)
|
|
5879
|
+
};
|
|
5880
|
+
}
|
|
5881
|
+
function extractAgentAnswerText(response) {
|
|
5882
|
+
const parts = [];
|
|
5883
|
+
for (const item of agentOutput(response)) {
|
|
5884
|
+
if (item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
5885
|
+
for (const part of item.content) {
|
|
5886
|
+
if (isRecord(part) && part.type === "output_text" && typeof part.text === "string") parts.push(part.text);
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
return parts.join("");
|
|
5890
|
+
}
|
|
5891
|
+
function extractAgentGroundingSources(response) {
|
|
5892
|
+
const sources = [];
|
|
5893
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5894
|
+
const add = (entry) => {
|
|
5895
|
+
if (!isRecord(entry) || typeof entry.url !== "string" || entry.url.length === 0) return;
|
|
5896
|
+
if (seen.has(entry.url)) return;
|
|
5897
|
+
seen.add(entry.url);
|
|
5898
|
+
sources.push({ uri: entry.url, title: typeof entry.title === "string" ? entry.title : "" });
|
|
5899
|
+
};
|
|
5900
|
+
for (const item of agentOutput(response)) {
|
|
5901
|
+
if (item.type === "search_results" && Array.isArray(item.results)) {
|
|
5902
|
+
item.results.forEach(add);
|
|
5903
|
+
} else if (item.type === "fetch_url_results" && Array.isArray(item.contents)) {
|
|
5904
|
+
item.contents.forEach(add);
|
|
5905
|
+
} else if (item.type === "message" && Array.isArray(item.content)) {
|
|
5906
|
+
for (const part of item.content) {
|
|
5907
|
+
if (isRecord(part) && Array.isArray(part.annotations)) part.annotations.forEach(add);
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
return sources;
|
|
5912
|
+
}
|
|
5913
|
+
function extractAgentSearchQueries(response) {
|
|
5914
|
+
const queries2 = /* @__PURE__ */ new Set();
|
|
5915
|
+
for (const item of agentOutput(response)) {
|
|
5916
|
+
if (item.type !== "search_results" || !Array.isArray(item.queries)) continue;
|
|
5917
|
+
for (const query of item.queries) {
|
|
5918
|
+
if (typeof query === "string" && query.trim().length > 0) queries2.add(query);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
return [...queries2];
|
|
5922
|
+
}
|
|
5923
|
+
function extractAgentRetrievalStatus(response) {
|
|
5924
|
+
const output = agentOutput(response);
|
|
5925
|
+
if (output.some((item) => typeof item.type === "string" && RETRIEVAL_OUTPUT_TYPES.has(item.type))) return "used";
|
|
5926
|
+
if (output.some((item) => item.type === "message")) return "not-used";
|
|
5927
|
+
return "unknown";
|
|
5928
|
+
}
|
|
5929
|
+
function parseSonarResponse(rawResponse) {
|
|
5808
5930
|
const groundingSources = extractGroundingSources(rawResponse);
|
|
5809
5931
|
return {
|
|
5810
5932
|
provider: "perplexity",
|
|
5811
5933
|
answerText: extractAnswerText3(rawResponse),
|
|
5812
5934
|
citedDomains: extractCitedDomains2(groundingSources),
|
|
5813
5935
|
groundingSources,
|
|
5814
|
-
//
|
|
5815
|
-
//
|
|
5936
|
+
// Sonar documented `search_results` and `citations` on the response but no
|
|
5937
|
+
// returned search-query telemetry, so Canonry does not synthesize it.
|
|
5816
5938
|
// Docs: https://docs.perplexity.ai/docs/sonar/openai-compatibility
|
|
5817
|
-
searchQueries: []
|
|
5939
|
+
searchQueries: [],
|
|
5940
|
+
// Sonar's only retrieval marker was present on every stored row, so it
|
|
5941
|
+
// never discriminated a non-retrieving answer. `unknown` is what we know.
|
|
5942
|
+
retrievalStatus: "unknown"
|
|
5818
5943
|
};
|
|
5819
5944
|
}
|
|
5820
|
-
function buildPrompt4(query, location) {
|
|
5821
|
-
if (location) {
|
|
5822
|
-
return `${query} (searching from ${location.city}, ${location.region}, ${location.country})`;
|
|
5823
|
-
}
|
|
5824
|
-
return query;
|
|
5825
|
-
}
|
|
5826
5945
|
function extractCitations(rawResponse) {
|
|
5827
5946
|
if (Array.isArray(rawResponse.citations)) {
|
|
5828
5947
|
return rawResponse.citations.filter((c) => typeof c === "string");
|
|
@@ -5865,9 +5984,7 @@ function extractSearchResults(rawResponse) {
|
|
|
5865
5984
|
function parseSearchResultsArray(value) {
|
|
5866
5985
|
if (!Array.isArray(value)) return [];
|
|
5867
5986
|
return value.flatMap((result2) => {
|
|
5868
|
-
if (
|
|
5869
|
-
return [];
|
|
5870
|
-
}
|
|
5987
|
+
if (!isRecord(result2)) return [];
|
|
5871
5988
|
const url = result2.url;
|
|
5872
5989
|
if (typeof url !== "string" || url.length === 0) {
|
|
5873
5990
|
return [];
|
|
@@ -5894,11 +6011,10 @@ function extractAnswerText3(rawResponse) {
|
|
|
5894
6011
|
}
|
|
5895
6012
|
}
|
|
5896
6013
|
function extractNestedApiResponse(rawResponse) {
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
return null;
|
|
6014
|
+
return isRecord(rawResponse.apiResponse) ? rawResponse.apiResponse : null;
|
|
6015
|
+
}
|
|
6016
|
+
function isRecord(value) {
|
|
6017
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5902
6018
|
}
|
|
5903
6019
|
function extractCitedDomains2(groundingSources) {
|
|
5904
6020
|
const domains = /* @__PURE__ */ new Set();
|
|
@@ -5914,22 +6030,11 @@ function extractDomainFromUri4(uri) {
|
|
|
5914
6030
|
return hostname;
|
|
5915
6031
|
}
|
|
5916
6032
|
async function generateText5(prompt, config) {
|
|
5917
|
-
const model = config.model
|
|
5918
|
-
const client =
|
|
5919
|
-
const response = await withRetry6(
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
messages: [{ role: "user", content: prompt }]
|
|
5923
|
-
})
|
|
5924
|
-
);
|
|
5925
|
-
return response.choices[0]?.message?.content ?? "";
|
|
5926
|
-
}
|
|
5927
|
-
function responseToRecord5(response) {
|
|
5928
|
-
try {
|
|
5929
|
-
return JSON.parse(JSON.stringify(response));
|
|
5930
|
-
} catch {
|
|
5931
|
-
return { error: "failed to serialize response" };
|
|
5932
|
-
}
|
|
6033
|
+
const model = resolveModel3(config.model);
|
|
6034
|
+
const client = createClient4(config.apiKey);
|
|
6035
|
+
const response = await withRetry6(() => postAgent(client, { ...agentSelection(model), input: prompt }));
|
|
6036
|
+
assertUsableResponse(response);
|
|
6037
|
+
return extractAgentAnswerText(response);
|
|
5933
6038
|
}
|
|
5934
6039
|
|
|
5935
6040
|
// ../provider-perplexity/src/adapter.ts
|
|
@@ -5944,19 +6049,22 @@ var perplexityAdapter = {
|
|
|
5944
6049
|
name: "perplexity",
|
|
5945
6050
|
displayName: "Perplexity",
|
|
5946
6051
|
mode: "api",
|
|
5947
|
-
// normalize.ts
|
|
6052
|
+
// normalize.ts sends the location as `user_location` on the web_search tool.
|
|
5948
6053
|
supportsLocationContext: true,
|
|
5949
6054
|
keyUrl: "https://www.perplexity.ai/settings/api",
|
|
5950
|
-
//
|
|
6055
|
+
// Agent API presets: https://docs.perplexity.ai/docs/agent-api/presets
|
|
6056
|
+
// Model slugs (GET https://api.perplexity.ai/v1/models): provider-prefixed, e.g. perplexity/sonar.
|
|
5951
6057
|
modelRegistry: {
|
|
5952
|
-
defaultModel:
|
|
5953
|
-
|
|
5954
|
-
|
|
6058
|
+
defaultModel: DEFAULT_MODEL5,
|
|
6059
|
+
// Presets, `vendor/model` slugs, and the retired Sonar names and previous
|
|
6060
|
+
// preset names that PROVIDER_MODEL_ALIASES resolves (`sonar` → `fast`).
|
|
6061
|
+
validationPattern: /^(?:fast|low|medium|high|xhigh|fast-search|pro-search|deep-research|advanced-deep-research|sonar(?:-pro|-reasoning|-reasoning-pro|-deep-research)?|[a-z0-9][a-z0-9-]*\/[A-Za-z0-9][\w.:-]*)$/,
|
|
6062
|
+
validationHint: "expected an Agent API preset (fast, low, medium, high, xhigh) or a provider/model slug (e.g. perplexity/sonar)",
|
|
5955
6063
|
knownModels: [
|
|
5956
|
-
{ id: "
|
|
5957
|
-
{ id: "
|
|
5958
|
-
{ id: "
|
|
5959
|
-
{ id: "sonar
|
|
6064
|
+
{ id: "fast", displayName: "Fast preset", tier: "standard" },
|
|
6065
|
+
{ id: "low", displayName: "Low preset (pro search)", tier: "flagship" },
|
|
6066
|
+
{ id: "medium", displayName: "Medium preset (deep research)", tier: "flagship" },
|
|
6067
|
+
{ id: "perplexity/sonar", displayName: "Sonar model + web search", tier: "fast" }
|
|
5960
6068
|
]
|
|
5961
6069
|
},
|
|
5962
6070
|
validateConfig(config) {
|
|
@@ -5992,13 +6100,10 @@ var perplexityAdapter = {
|
|
|
5992
6100
|
servedModel: raw.servedModel,
|
|
5993
6101
|
groundingSources: raw.groundingSources,
|
|
5994
6102
|
searchQueries: raw.searchQueries,
|
|
5995
|
-
//
|
|
5996
|
-
//
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
// declaration about how we build the request, so it is always knowable.
|
|
6000
|
-
retrievalStatus: "unknown",
|
|
6001
|
-
retrievalContract: "native-auto-v1"
|
|
6103
|
+
// Read off the Agent API output: a `search_results` item is the retrieval
|
|
6104
|
+
// call. The contract is how we built the request, so it is always known.
|
|
6105
|
+
retrievalStatus: raw.retrievalStatus ?? "unknown",
|
|
6106
|
+
retrievalContract: PERPLEXITY_RETRIEVAL_CONTRACT
|
|
6002
6107
|
};
|
|
6003
6108
|
},
|
|
6004
6109
|
normalizeResult(raw) {
|
|
@@ -6007,7 +6112,8 @@ var perplexityAdapter = {
|
|
|
6007
6112
|
rawResponse: raw.rawResponse,
|
|
6008
6113
|
model: raw.model,
|
|
6009
6114
|
groundingSources: raw.groundingSources,
|
|
6010
|
-
searchQueries: raw.searchQueries
|
|
6115
|
+
searchQueries: raw.searchQueries,
|
|
6116
|
+
retrievalStatus: raw.retrievalStatus
|
|
6011
6117
|
};
|
|
6012
6118
|
const normalized = normalizeResult6(perplexityRaw);
|
|
6013
6119
|
return {
|
|
@@ -6016,7 +6122,7 @@ var perplexityAdapter = {
|
|
|
6016
6122
|
citedDomains: normalized.citedDomains,
|
|
6017
6123
|
groundingSources: normalized.groundingSources,
|
|
6018
6124
|
searchQueries: normalized.searchQueries,
|
|
6019
|
-
retrievalStatus:
|
|
6125
|
+
retrievalStatus: normalized.retrievalStatus
|
|
6020
6126
|
};
|
|
6021
6127
|
},
|
|
6022
6128
|
async generateText(prompt, config) {
|
|
@@ -6498,7 +6604,7 @@ function nonBlank(value) {
|
|
|
6498
6604
|
return trimmed ? trimmed : void 0;
|
|
6499
6605
|
}
|
|
6500
6606
|
function resolveBuildCommit(env = process.env) {
|
|
6501
|
-
const embedded = true ? "
|
|
6607
|
+
const embedded = true ? "b5366056cfff6f0e275fe651252cfa1961bf40a9" : void 0;
|
|
6502
6608
|
return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
|
|
6503
6609
|
}
|
|
6504
6610
|
function resolveInstanceIdentity(env = process.env) {
|
|
@@ -6964,7 +7070,8 @@ var JobRunner = class {
|
|
|
6964
7070
|
} else {
|
|
6965
7071
|
const projectProviders = providerOverride ?? project.providers;
|
|
6966
7072
|
activeProviders = this.registry.getForProject(projectProviders).map((entry) => {
|
|
6967
|
-
const
|
|
7073
|
+
const override = project.providerModels[entry.adapter.name];
|
|
7074
|
+
const model = override === void 0 ? void 0 : resolveProviderModel(entry.adapter.name, override);
|
|
6968
7075
|
return model === void 0 ? entry : { ...entry, config: { ...entry.config, model } };
|
|
6969
7076
|
});
|
|
6970
7077
|
}
|
|
@@ -7444,15 +7551,15 @@ var JobRunner = class {
|
|
|
7444
7551
|
const dispatchable = [];
|
|
7445
7552
|
for (const registered of activeProviders) {
|
|
7446
7553
|
const name = registered.adapter.name;
|
|
7447
|
-
const
|
|
7554
|
+
const count2 = unitsByProvider.get(name)?.length ?? 0;
|
|
7448
7555
|
const scope = `${projectId}:${name}`;
|
|
7449
7556
|
const limit = registered.config.quotaPolicy.maxRequestsPerDay;
|
|
7450
|
-
const quota = reserveDailyQueryQuota(this.db, { scope, period: todayPeriod, count, limit });
|
|
7557
|
+
const quota = reserveDailyQueryQuota(this.db, { scope, period: todayPeriod, count: count2, limit });
|
|
7451
7558
|
if (!quota.reserved) {
|
|
7452
|
-
providerErrors.set(name, `Daily quota exceeded for ${name}: ${quota.used} queries used today, limit is ${limit}. This fill needs ${
|
|
7559
|
+
providerErrors.set(name, `Daily quota exceeded for ${name}: ${quota.used} queries used today, limit is ${limit}. This fill needs ${count2} more.`);
|
|
7453
7560
|
continue;
|
|
7454
7561
|
}
|
|
7455
|
-
providerReservations.set(name, { scope, period: todayPeriod, reserved:
|
|
7562
|
+
providerReservations.set(name, { scope, period: todayPeriod, reserved: count2 });
|
|
7456
7563
|
dispatchable.push(registered);
|
|
7457
7564
|
}
|
|
7458
7565
|
const executionGates = /* @__PURE__ */ new Map();
|
|
@@ -7565,8 +7672,8 @@ var JobRunner = class {
|
|
|
7565
7672
|
const remaining = /* @__PURE__ */ new Map();
|
|
7566
7673
|
for (const slot of state.missing) remaining.set(slot.provider, (remaining.get(slot.provider) ?? 0) + 1);
|
|
7567
7674
|
const providers = {};
|
|
7568
|
-
for (const [provider,
|
|
7569
|
-
providers[provider] = fresh[provider] ?? previous[provider] ?? { message: `${
|
|
7675
|
+
for (const [provider, count2] of remaining) {
|
|
7676
|
+
providers[provider] = fresh[provider] ?? previous[provider] ?? { message: `${count2} expected measurement(s) have not run.` };
|
|
7570
7677
|
}
|
|
7571
7678
|
txDb.update(runs).set({ error: serializeRunError({ providers }) }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run();
|
|
7572
7679
|
}
|
|
@@ -7735,7 +7842,7 @@ var JobRunner = class {
|
|
|
7735
7842
|
ctx.fill?.onOutcome(providerName, false);
|
|
7736
7843
|
}
|
|
7737
7844
|
}
|
|
7738
|
-
incrementUsage(scope, metric,
|
|
7845
|
+
incrementUsage(scope, metric, count2) {
|
|
7739
7846
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7740
7847
|
const period = now.slice(0, 10);
|
|
7741
7848
|
this.db.insert(usageCounters).values({
|
|
@@ -7743,11 +7850,11 @@ var JobRunner = class {
|
|
|
7743
7850
|
scope,
|
|
7744
7851
|
period,
|
|
7745
7852
|
metric,
|
|
7746
|
-
count,
|
|
7853
|
+
count: count2,
|
|
7747
7854
|
updatedAt: now
|
|
7748
7855
|
}).onConflictDoUpdate({
|
|
7749
7856
|
target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
|
|
7750
|
-
set: { count: sql2`${usageCounters.count} + ${
|
|
7857
|
+
set: { count: sql2`${usageCounters.count} + ${count2}`, updatedAt: now }
|
|
7751
7858
|
}).run();
|
|
7752
7859
|
}
|
|
7753
7860
|
flushProviderUsage(providerDispatchCounts, providerReservations) {
|
|
@@ -8471,11 +8578,11 @@ function validateGeoSearchLimit(limit) {
|
|
|
8471
8578
|
);
|
|
8472
8579
|
}
|
|
8473
8580
|
}
|
|
8474
|
-
function
|
|
8581
|
+
function isRecord2(value) {
|
|
8475
8582
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8476
8583
|
}
|
|
8477
8584
|
function validateRequestObject(value, label) {
|
|
8478
|
-
if (!
|
|
8585
|
+
if (!isRecord2(value)) {
|
|
8479
8586
|
throw new OpenAiAdsApiError(`${label} must be a JSON object`, 400);
|
|
8480
8587
|
}
|
|
8481
8588
|
}
|
|
@@ -8634,7 +8741,7 @@ function validateLandingPageConfiguration(value, label) {
|
|
|
8634
8741
|
}
|
|
8635
8742
|
}
|
|
8636
8743
|
function parseLandingPageQueryStringTemplate(value) {
|
|
8637
|
-
if (!
|
|
8744
|
+
if (!isRecord2(value)) return null;
|
|
8638
8745
|
const template = value.query_string_template;
|
|
8639
8746
|
return typeof template === "string" && template.trim().length > 0 ? template : null;
|
|
8640
8747
|
}
|
|
@@ -12074,7 +12181,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
12074
12181
|
return result2;
|
|
12075
12182
|
}
|
|
12076
12183
|
async function backfillInsightsCommand(project, opts) {
|
|
12077
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
12184
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-MKHINDHN.js");
|
|
12078
12185
|
const config = loadConfig();
|
|
12079
12186
|
const db = createClient(config.database);
|
|
12080
12187
|
migrate(db);
|
|
@@ -12588,8 +12695,8 @@ Traffic classification ${result2.dryRun ? "preview" : "recovery"} complete.`);
|
|
|
12588
12695
|
if (result2.reclassified > 0) {
|
|
12589
12696
|
console.log(` By bot:`);
|
|
12590
12697
|
const sorted = Object.entries(result2.byBot).sort(([, a], [, b]) => b - a);
|
|
12591
|
-
for (const [bot,
|
|
12592
|
-
console.log(` ${
|
|
12698
|
+
for (const [bot, count2] of sorted) {
|
|
12699
|
+
console.log(` ${count2.toString().padStart(5)} ${bot}`);
|
|
12593
12700
|
}
|
|
12594
12701
|
}
|
|
12595
12702
|
if (result2.dryRun) {
|
|
@@ -12602,7 +12709,8 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
12602
12709
|
var ProviderRegistry = class {
|
|
12603
12710
|
providers = /* @__PURE__ */ new Map();
|
|
12604
12711
|
register(adapter, config) {
|
|
12605
|
-
|
|
12712
|
+
const resolved = config.model === void 0 ? config : { ...config, model: resolveProviderModel(adapter.name, config.model) };
|
|
12713
|
+
this.providers.set(adapter.name, { adapter, config: resolved });
|
|
12606
12714
|
}
|
|
12607
12715
|
get(name) {
|
|
12608
12716
|
return this.providers.get(name);
|
|
@@ -13928,7 +14036,7 @@ function analysisResultFromInsights(insights2) {
|
|
|
13928
14036
|
|
|
13929
14037
|
// src/agent/session-registry.ts
|
|
13930
14038
|
import crypto24 from "crypto";
|
|
13931
|
-
import { eq as
|
|
14039
|
+
import { eq as eq21 } from "drizzle-orm";
|
|
13932
14040
|
|
|
13933
14041
|
// src/agent/session.ts
|
|
13934
14042
|
import fs7 from "fs";
|
|
@@ -14301,12 +14409,26 @@ import { Type as Type3 } from "@sinclair/typebox";
|
|
|
14301
14409
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
14302
14410
|
import { randomUUID } from "crypto";
|
|
14303
14411
|
var MAX_TOOL_RESULT_CHARS = 2e4;
|
|
14304
|
-
var TRUNCATION_NOTE = "... (truncated
|
|
14412
|
+
var TRUNCATION_NOTE = "... (truncated, result too large)";
|
|
14413
|
+
var TRUNCATION_SUMMARY_KEY = "__truncation";
|
|
14305
14414
|
var MAX_STRUCTURED_TRUNCATION_CHARS = 2e6;
|
|
14306
|
-
var
|
|
14415
|
+
var TRUNCATION_PASS_BUDGET_CHARS = 64e6;
|
|
14416
|
+
var MIN_TRUNCATION_PASSES = 32;
|
|
14417
|
+
var MAX_TRUNCATION_PASSES = 200;
|
|
14418
|
+
var MAX_SUMMARY_DEPTH = 5;
|
|
14419
|
+
var MAX_SUMMARY_KEYS = 30;
|
|
14420
|
+
var MAX_SUMMARY_SEGMENT_CHARS = 60;
|
|
14307
14421
|
function serializeResult(value) {
|
|
14308
14422
|
return JSON.stringify(value, null, 2);
|
|
14309
14423
|
}
|
|
14424
|
+
function keptSummary(counts) {
|
|
14425
|
+
const summary = { droppedKeys: [], keptItems: {} };
|
|
14426
|
+
for (const [path9, { kept, total }] of counts) {
|
|
14427
|
+
if (kept === 0) summary.droppedKeys.push(path9);
|
|
14428
|
+
summary.keptItems[path9] = `${kept} of ${total}`;
|
|
14429
|
+
}
|
|
14430
|
+
return summary;
|
|
14431
|
+
}
|
|
14310
14432
|
function largestArrayKey(obj) {
|
|
14311
14433
|
let best;
|
|
14312
14434
|
let bestLen = -1;
|
|
@@ -14320,117 +14442,278 @@ function largestArrayKey(obj) {
|
|
|
14320
14442
|
}
|
|
14321
14443
|
return best;
|
|
14322
14444
|
}
|
|
14323
|
-
function
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14445
|
+
function fitsCap(text) {
|
|
14446
|
+
return text.length <= MAX_TOOL_RESULT_CHARS;
|
|
14447
|
+
}
|
|
14448
|
+
function largestFitting(max, fits) {
|
|
14327
14449
|
let low = 0;
|
|
14328
|
-
let high =
|
|
14450
|
+
let high = max;
|
|
14329
14451
|
while (low < high) {
|
|
14330
14452
|
const middle = Math.ceil((low + high) / 2);
|
|
14331
|
-
if (
|
|
14453
|
+
if (fits(middle)) low = middle;
|
|
14332
14454
|
else high = middle - 1;
|
|
14333
14455
|
}
|
|
14334
|
-
return
|
|
14456
|
+
return low;
|
|
14335
14457
|
}
|
|
14336
|
-
function
|
|
14337
|
-
|
|
14338
|
-
const
|
|
14458
|
+
function trimRowsToFit(rows, render) {
|
|
14459
|
+
if (rows.length === 0) return [];
|
|
14460
|
+
const all = rows.slice();
|
|
14461
|
+
if (fitsCap(render(all))) return all;
|
|
14462
|
+
return rows.slice(0, largestFitting(rows.length - 1, (count2) => fitsCap(render(rows.slice(0, count2)))));
|
|
14463
|
+
}
|
|
14464
|
+
function nestedArrayGroups(root) {
|
|
14465
|
+
const groups = /* @__PURE__ */ new Map();
|
|
14466
|
+
const visit = (object, path9) => {
|
|
14339
14467
|
let containsRecordCollection = false;
|
|
14340
14468
|
for (const [key, value] of Object.entries(object)) {
|
|
14469
|
+
if (object === root && key === TRUNCATION_SUMMARY_KEY) continue;
|
|
14470
|
+
const valuePath = path9 ? `${path9}.${key}` : key;
|
|
14341
14471
|
if (Array.isArray(value)) {
|
|
14342
14472
|
let hasNestedRecords = false;
|
|
14343
14473
|
let hasRecordRows = false;
|
|
14344
14474
|
for (const row of value) {
|
|
14345
14475
|
if (row && typeof row === "object" && !Array.isArray(row)) {
|
|
14346
14476
|
hasRecordRows = true;
|
|
14347
|
-
hasNestedRecords = visit(row) || hasNestedRecords;
|
|
14477
|
+
hasNestedRecords = visit(row, `${valuePath}[]`) || hasNestedRecords;
|
|
14348
14478
|
}
|
|
14349
14479
|
}
|
|
14350
14480
|
if (value.length > 0 && !hasNestedRecords) {
|
|
14351
|
-
|
|
14481
|
+
const id = `${Number(hasRecordRows)}:${valuePath}`;
|
|
14482
|
+
const group = groups.get(id) ?? { path: valuePath, members: [], size: 0, hasRecordRows };
|
|
14483
|
+
group.members.push({ owner: object, key, rows: value });
|
|
14484
|
+
group.size += serializeResult(value).length;
|
|
14485
|
+
groups.set(id, group);
|
|
14352
14486
|
}
|
|
14353
14487
|
containsRecordCollection = hasRecordRows || hasNestedRecords || containsRecordCollection;
|
|
14354
14488
|
} else if (value && typeof value === "object") {
|
|
14355
|
-
containsRecordCollection = visit(value) || containsRecordCollection;
|
|
14489
|
+
containsRecordCollection = visit(value, valuePath) || containsRecordCollection;
|
|
14356
14490
|
}
|
|
14357
14491
|
}
|
|
14358
14492
|
return containsRecordCollection;
|
|
14359
14493
|
};
|
|
14360
|
-
visit(root);
|
|
14361
|
-
return
|
|
14494
|
+
visit(root, "");
|
|
14495
|
+
return [...groups.values()].sort(
|
|
14362
14496
|
(left, right) => Number(right.hasRecordRows) - Number(left.hasRecordRows) || right.size - left.size
|
|
14363
14497
|
);
|
|
14364
14498
|
}
|
|
14499
|
+
function memberTrimmer({ owner, key, rows }) {
|
|
14500
|
+
const hadTruncated = Object.hasOwn(owner, "__truncated");
|
|
14501
|
+
const priorTruncated = owner.__truncated;
|
|
14502
|
+
const previous = owner.__omittedRowsByField;
|
|
14503
|
+
const priorOmitted = previous && typeof previous === "object" && !Array.isArray(previous) ? previous : void 0;
|
|
14504
|
+
const priorValue = priorOmitted?.[key];
|
|
14505
|
+
const priorCount = typeof priorValue === "number" ? priorValue : 0;
|
|
14506
|
+
return (count2) => {
|
|
14507
|
+
const kept = Math.min(count2, rows.length);
|
|
14508
|
+
owner[key] = rows.slice(0, kept);
|
|
14509
|
+
const omitted = priorCount + rows.length - kept;
|
|
14510
|
+
if (omitted > 0) {
|
|
14511
|
+
owner.__truncated = true;
|
|
14512
|
+
owner.__omittedRowsByField = { ...priorOmitted, [key]: omitted };
|
|
14513
|
+
return kept;
|
|
14514
|
+
}
|
|
14515
|
+
if (hadTruncated) owner.__truncated = priorTruncated;
|
|
14516
|
+
else delete owner.__truncated;
|
|
14517
|
+
if (previous === void 0) delete owner.__omittedRowsByField;
|
|
14518
|
+
else owner.__omittedRowsByField = previous;
|
|
14519
|
+
return kept;
|
|
14520
|
+
};
|
|
14521
|
+
}
|
|
14522
|
+
function emptiedFloorChars(root) {
|
|
14523
|
+
return JSON.stringify(root, (_key, value) => Array.isArray(value) ? [] : value, 2).length;
|
|
14524
|
+
}
|
|
14365
14525
|
function trimNestedArrays(full) {
|
|
14366
14526
|
const parsed = JSON.parse(full);
|
|
14367
14527
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
14368
14528
|
const copy = parsed;
|
|
14529
|
+
const counts = /* @__PURE__ */ new Map();
|
|
14369
14530
|
copy.__truncated = true;
|
|
14531
|
+
copy[TRUNCATION_SUMMARY_KEY] = keptSummary(counts);
|
|
14532
|
+
if (emptiedFloorChars(copy) > MAX_TOOL_RESULT_CHARS) return void 0;
|
|
14370
14533
|
let out = serializeResult(copy);
|
|
14534
|
+
const passLimit = Math.min(
|
|
14535
|
+
MAX_TRUNCATION_PASSES,
|
|
14536
|
+
Math.max(MIN_TRUNCATION_PASSES, Math.floor(TRUNCATION_PASS_BUDGET_CHARS / full.length))
|
|
14537
|
+
);
|
|
14371
14538
|
let passes = 0;
|
|
14372
|
-
while (out
|
|
14373
|
-
if (++passes >
|
|
14374
|
-
const
|
|
14375
|
-
if (!
|
|
14376
|
-
const
|
|
14377
|
-
const
|
|
14378
|
-
const
|
|
14379
|
-
const
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
omitted[key] = priorCount + rows.length - count;
|
|
14539
|
+
while (!fitsCap(out)) {
|
|
14540
|
+
if (++passes > passLimit) return void 0;
|
|
14541
|
+
const group = nestedArrayGroups(copy).at(0);
|
|
14542
|
+
if (!group) return void 0;
|
|
14543
|
+
const trimmers = group.members.map(memberTrimmer);
|
|
14544
|
+
const total = group.members.reduce((sum, member) => sum + member.rows.length, 0);
|
|
14545
|
+
const longest = group.members.reduce((max, member) => Math.max(max, member.rows.length), 0);
|
|
14546
|
+
const prior = counts.get(group.path) ?? { kept: 0, total: 0 };
|
|
14547
|
+
const keep = (count3, extra2 = 0) => {
|
|
14548
|
+
const kept = trimmers.reduce((sum, trim, index) => sum + trim(index < extra2 ? count3 + 1 : count3), 0);
|
|
14549
|
+
counts.set(group.path, { kept: prior.kept + kept, total: prior.total + total });
|
|
14550
|
+
copy[TRUNCATION_SUMMARY_KEY] = keptSummary(counts);
|
|
14385
14551
|
return serializeResult(copy);
|
|
14386
14552
|
};
|
|
14387
14553
|
out = keep(0);
|
|
14388
|
-
if (out
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
const middle = Math.ceil((low + high) / 2);
|
|
14393
|
-
if (keep(middle).length <= MAX_TOOL_RESULT_CHARS) low = middle;
|
|
14394
|
-
else high = middle - 1;
|
|
14395
|
-
}
|
|
14396
|
-
out = keep(low);
|
|
14554
|
+
if (!fitsCap(out)) continue;
|
|
14555
|
+
const count2 = largestFitting(longest - 1, (n) => fitsCap(keep(n)));
|
|
14556
|
+
const extra = trimmers.length > 1 ? largestFitting(trimmers.length - 1, (n) => fitsCap(keep(count2, n))) : 0;
|
|
14557
|
+
out = keep(count2, extra);
|
|
14397
14558
|
}
|
|
14398
14559
|
return out;
|
|
14399
14560
|
}
|
|
14561
|
+
function shortSegment(key) {
|
|
14562
|
+
return key.length > MAX_SUMMARY_SEGMENT_CHARS ? `${key.slice(0, MAX_SUMMARY_SEGMENT_CHARS)}...` : key;
|
|
14563
|
+
}
|
|
14564
|
+
function childPath(container) {
|
|
14565
|
+
if (container.kind === "array") return `${container.path}[${container.complete}]`;
|
|
14566
|
+
const key = shortSegment(container.key ?? "");
|
|
14567
|
+
return container.path ? `${container.path}.${key}` : key;
|
|
14568
|
+
}
|
|
14569
|
+
function serializedKeys(value) {
|
|
14570
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
14571
|
+
const record = value;
|
|
14572
|
+
if (typeof record.toJSON === "function") return void 0;
|
|
14573
|
+
return Object.keys(record).filter((key) => {
|
|
14574
|
+
const child = record[key];
|
|
14575
|
+
return child !== void 0 && typeof child !== "function" && typeof child !== "symbol";
|
|
14576
|
+
});
|
|
14577
|
+
}
|
|
14578
|
+
function openContainersAt(shown, details) {
|
|
14579
|
+
const stack = [];
|
|
14580
|
+
let inString = false;
|
|
14581
|
+
let escaped = false;
|
|
14582
|
+
let stringStart = 0;
|
|
14583
|
+
for (let i = 0; i < shown.length; i++) {
|
|
14584
|
+
const ch = shown[i];
|
|
14585
|
+
const top = stack.at(-1);
|
|
14586
|
+
if (inString) {
|
|
14587
|
+
if (escaped) escaped = false;
|
|
14588
|
+
else if (ch === "\\") escaped = true;
|
|
14589
|
+
else if (ch === '"') {
|
|
14590
|
+
inString = false;
|
|
14591
|
+
if (top?.kind === "object" && top.expectKey) {
|
|
14592
|
+
top.key = JSON.parse(shown.slice(stringStart, i + 1));
|
|
14593
|
+
top.expectKey = false;
|
|
14594
|
+
}
|
|
14595
|
+
}
|
|
14596
|
+
continue;
|
|
14597
|
+
}
|
|
14598
|
+
if (ch === '"') {
|
|
14599
|
+
inString = true;
|
|
14600
|
+
stringStart = i;
|
|
14601
|
+
} else if (ch === "{" || ch === "[") {
|
|
14602
|
+
let value = details;
|
|
14603
|
+
if (top) {
|
|
14604
|
+
const parent = top.value;
|
|
14605
|
+
value = top.kind === "array" ? Array.isArray(parent) ? parent[top.complete] : void 0 : parent && typeof parent === "object" ? parent[top.key ?? ""] : void 0;
|
|
14606
|
+
}
|
|
14607
|
+
stack.push({
|
|
14608
|
+
kind: ch === "{" ? "object" : "array",
|
|
14609
|
+
path: top ? childPath(top) : "",
|
|
14610
|
+
value,
|
|
14611
|
+
complete: 0,
|
|
14612
|
+
childClosed: false,
|
|
14613
|
+
expectKey: ch === "{"
|
|
14614
|
+
});
|
|
14615
|
+
} else if (ch === "}" || ch === "]") {
|
|
14616
|
+
stack.pop();
|
|
14617
|
+
const parent = stack.at(-1);
|
|
14618
|
+
if (parent) parent.childClosed = true;
|
|
14619
|
+
} else if (ch === "," && top) {
|
|
14620
|
+
top.complete++;
|
|
14621
|
+
top.childClosed = false;
|
|
14622
|
+
top.expectKey = top.kind === "object";
|
|
14623
|
+
}
|
|
14624
|
+
}
|
|
14625
|
+
return stack;
|
|
14626
|
+
}
|
|
14627
|
+
function sliceSummaryLine(shown, details) {
|
|
14628
|
+
if (!shown.startsWith("{") && !shown.startsWith("[")) return "";
|
|
14629
|
+
const stack = openContainersAt(shown, details);
|
|
14630
|
+
const root = stack.at(0);
|
|
14631
|
+
const deepest = stack.at(-1);
|
|
14632
|
+
if (!root || !deepest) return "";
|
|
14633
|
+
const summary = {
|
|
14634
|
+
// Between two children the cut is named by their container.
|
|
14635
|
+
cutAt: deepest.childClosed || deepest.expectKey ? deepest.path : childPath(deepest),
|
|
14636
|
+
droppedKeys: [],
|
|
14637
|
+
keptItems: {}
|
|
14638
|
+
};
|
|
14639
|
+
for (const container of stack.slice(0, MAX_SUMMARY_DEPTH)) {
|
|
14640
|
+
const total = container.kind === "array" ? Array.isArray(container.value) ? container.value.length : void 0 : serializedKeys(container.value)?.length;
|
|
14641
|
+
if (total === void 0) break;
|
|
14642
|
+
if (container === root && container.kind === "object") continue;
|
|
14643
|
+
const shownCount = container.complete + (container.childClosed ? 1 : 0);
|
|
14644
|
+
summary.keptItems[container.path || "(root)"] = container.kind === "array" ? `${shownCount} of ${total}` : `${shownCount} of ${total} keys`;
|
|
14645
|
+
}
|
|
14646
|
+
const rootKeys = root.kind === "object" ? serializedKeys(root.value) : void 0;
|
|
14647
|
+
if (rootKeys) {
|
|
14648
|
+
const inProgress = root.expectKey || root.childClosed ? 0 : 1;
|
|
14649
|
+
const dropped = rootKeys.slice(root.complete + (root.childClosed ? 1 : 0) + inProgress);
|
|
14650
|
+
const record = root.value;
|
|
14651
|
+
for (const key of dropped.slice(0, MAX_SUMMARY_KEYS)) {
|
|
14652
|
+
const label = shortSegment(key);
|
|
14653
|
+
summary.droppedKeys.push(label);
|
|
14654
|
+
const value = record[key];
|
|
14655
|
+
if (Array.isArray(value)) summary.keptItems[label] = `0 of ${value.length}`;
|
|
14656
|
+
}
|
|
14657
|
+
if (dropped.length > MAX_SUMMARY_KEYS) summary.moreDroppedKeys = dropped.length - MAX_SUMMARY_KEYS;
|
|
14658
|
+
}
|
|
14659
|
+
return `${TRUNCATION_SUMMARY_KEY}: ${JSON.stringify(summary)}`;
|
|
14660
|
+
}
|
|
14661
|
+
function markedSlice(full, details) {
|
|
14662
|
+
let cut = MAX_TOOL_RESULT_CHARS;
|
|
14663
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
14664
|
+
const line = sliceSummaryLine(full.slice(0, cut), details);
|
|
14665
|
+
const cost = line ? line.length + 1 : 0;
|
|
14666
|
+
if (cut + cost <= MAX_TOOL_RESULT_CHARS) {
|
|
14667
|
+
return full.slice(0, cut) + "\n" + (line ? line + "\n" : "") + TRUNCATION_NOTE;
|
|
14668
|
+
}
|
|
14669
|
+
cut = MAX_TOOL_RESULT_CHARS - cost - attempt * 16;
|
|
14670
|
+
}
|
|
14671
|
+
return full.slice(0, MAX_TOOL_RESULT_CHARS) + "\n" + TRUNCATION_NOTE;
|
|
14672
|
+
}
|
|
14400
14673
|
function truncateToolResult(details) {
|
|
14401
14674
|
const full = serializeResult(details);
|
|
14402
14675
|
if (full.length <= MAX_TOOL_RESULT_CHARS) return full;
|
|
14403
|
-
if (full.length > MAX_STRUCTURED_TRUNCATION_CHARS)
|
|
14404
|
-
return full.slice(0, MAX_TOOL_RESULT_CHARS) + "\n" + TRUNCATION_NOTE;
|
|
14405
|
-
}
|
|
14676
|
+
if (full.length > MAX_STRUCTURED_TRUNCATION_CHARS) return markedSlice(full, details);
|
|
14406
14677
|
if (Array.isArray(details)) {
|
|
14407
|
-
const
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14678
|
+
const render = (rows) => serializeResult({
|
|
14679
|
+
items: rows,
|
|
14680
|
+
__truncated: true,
|
|
14681
|
+
__omittedRows: details.length - rows.length,
|
|
14682
|
+
[TRUNCATION_SUMMARY_KEY]: keptSummary(/* @__PURE__ */ new Map([["items", { kept: rows.length, total: details.length }]]))
|
|
14683
|
+
});
|
|
14684
|
+
return render(trimRowsToFit(details, render));
|
|
14412
14685
|
}
|
|
14413
14686
|
if (details && typeof details === "object") {
|
|
14414
14687
|
const obj = details;
|
|
14415
14688
|
const arrayKey = largestArrayKey(obj);
|
|
14416
14689
|
if (arrayKey) {
|
|
14417
14690
|
const rows = obj[arrayKey];
|
|
14418
|
-
const
|
|
14419
|
-
rows,
|
|
14420
|
-
(r) => serializeResult({ ...obj, [arrayKey]: r, __truncated: true, __omittedRows: rows.length - r.length })
|
|
14421
|
-
);
|
|
14422
|
-
const out = serializeResult({
|
|
14691
|
+
const render = (kept) => serializeResult({
|
|
14423
14692
|
...obj,
|
|
14424
14693
|
[arrayKey]: kept,
|
|
14425
14694
|
__truncated: true,
|
|
14426
|
-
__omittedRows: rows.length - kept.length
|
|
14695
|
+
__omittedRows: rows.length - kept.length,
|
|
14696
|
+
[TRUNCATION_SUMMARY_KEY]: keptSummary(/* @__PURE__ */ new Map([[arrayKey, { kept: kept.length, total: rows.length }]]))
|
|
14427
14697
|
});
|
|
14428
|
-
|
|
14698
|
+
const out = render(trimRowsToFit(rows, render));
|
|
14699
|
+
const otherLists = Object.entries(obj).some(([key, value]) => key !== arrayKey && Array.isArray(value) && value.length > 0);
|
|
14700
|
+
const nested2 = otherLists ? trimNestedArrays(full) : void 0;
|
|
14701
|
+
if (fitsCap(out) && nested2 !== void 0 && keptRows(nested2, arrayKey) > keptRows(out, arrayKey)) return nested2;
|
|
14702
|
+
if (fitsCap(out)) return out;
|
|
14703
|
+
if (nested2 !== void 0) return nested2;
|
|
14429
14704
|
}
|
|
14430
14705
|
const nested = trimNestedArrays(full);
|
|
14431
14706
|
if (nested !== void 0) return nested;
|
|
14432
14707
|
}
|
|
14433
|
-
return full
|
|
14708
|
+
return markedSlice(full, details);
|
|
14709
|
+
}
|
|
14710
|
+
function keptRows(serialized, key) {
|
|
14711
|
+
try {
|
|
14712
|
+
const parsed = JSON.parse(serialized);
|
|
14713
|
+
return Array.isArray(parsed[key]) ? parsed[key].length : 0;
|
|
14714
|
+
} catch {
|
|
14715
|
+
return 0;
|
|
14716
|
+
}
|
|
14434
14717
|
}
|
|
14435
14718
|
function textResult2(details) {
|
|
14436
14719
|
return {
|
|
@@ -14835,11 +15118,11 @@ function recordLlmUsageEvent(args) {
|
|
|
14835
15118
|
var AERO_MEMORY_SEPARATOR = "\n\n---\n\n";
|
|
14836
15119
|
var AERO_MEMORY_BLOCK_PREFIX = `${AERO_MEMORY_SEPARATOR}<memory>
|
|
14837
15120
|
`;
|
|
14838
|
-
function
|
|
15121
|
+
function isRecord3(value) {
|
|
14839
15122
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
14840
15123
|
}
|
|
14841
15124
|
function isTextSystemBlock(value) {
|
|
14842
|
-
return
|
|
15125
|
+
return isRecord3(value) && value.type === "text" && typeof value.text === "string";
|
|
14843
15126
|
}
|
|
14844
15127
|
function splitAeroHydratedSystemPrompt(systemPrompt) {
|
|
14845
15128
|
const idx = systemPrompt.lastIndexOf(AERO_MEMORY_BLOCK_PREFIX);
|
|
@@ -14851,7 +15134,7 @@ function splitAeroHydratedSystemPrompt(systemPrompt) {
|
|
|
14851
15134
|
}
|
|
14852
15135
|
function splitAeroAnthropicSystemCachePayload(payload, model) {
|
|
14853
15136
|
if (model.api !== "anthropic-messages") return void 0;
|
|
14854
|
-
if (!
|
|
15137
|
+
if (!isRecord3(payload) || !Array.isArray(payload.system)) return void 0;
|
|
14855
15138
|
let changed = false;
|
|
14856
15139
|
const nextSystem = [];
|
|
14857
15140
|
for (const block of payload.system) {
|
|
@@ -14887,6 +15170,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
|
|
|
14887
15170
|
import { z } from "zod";
|
|
14888
15171
|
import { Type as Type4 } from "@sinclair/typebox";
|
|
14889
15172
|
var DISCOVER = "aero_list_toolkits";
|
|
15173
|
+
var MAX_VISIBLE_TOOLS = 128;
|
|
14890
15174
|
var LOAD = "aero_load_toolkit";
|
|
14891
15175
|
var metadata = new Map(canonryMcpTools.map((tool) => [tool.name, tool]));
|
|
14892
15176
|
var runtimes = /* @__PURE__ */ new WeakMap();
|
|
@@ -14897,7 +15181,7 @@ function visibleTools(runtime) {
|
|
|
14897
15181
|
if (!runtime.progressive) return runtime.allowed;
|
|
14898
15182
|
const visible = runtime.allowed.filter((tool) => {
|
|
14899
15183
|
const entry = metadata.get(tool.name);
|
|
14900
|
-
return !entry || entry.tier === "core" || runtime.loaded.has(entry.tier);
|
|
15184
|
+
return !entry || entry.tier === "core" || runtime.pinned.has(tool.name) || runtime.loaded.has(entry.tier);
|
|
14901
15185
|
});
|
|
14902
15186
|
const available = CANONRY_MCP_TOOLKITS.filter((kit) => runtime.allowed.some((tool) => metadata.get(tool.name)?.tier === kit.name));
|
|
14903
15187
|
return [...visible, {
|
|
@@ -14914,15 +15198,39 @@ function visibleTools(runtime) {
|
|
|
14914
15198
|
execute: async (_id, input) => {
|
|
14915
15199
|
const args = z.object({ toolkit: z.string() }).parse(input);
|
|
14916
15200
|
if (!available.some((kit) => kit.name === args.toolkit)) throw new Error("Toolkit is unavailable for this turn.");
|
|
15201
|
+
runtime.loaded.delete(args.toolkit);
|
|
14917
15202
|
runtime.loaded.add(args.toolkit);
|
|
14918
|
-
|
|
15203
|
+
const unloaded = [];
|
|
15204
|
+
for (const older of [...runtime.loaded]) {
|
|
15205
|
+
if (visibleTools(runtime).length <= MAX_VISIBLE_TOOLS || older === args.toolkit) continue;
|
|
15206
|
+
runtime.loaded.delete(older);
|
|
15207
|
+
unloaded.push(older);
|
|
15208
|
+
}
|
|
15209
|
+
return result({
|
|
15210
|
+
tools: runtime.allowed.filter((tool) => metadata.get(tool.name)?.tier === args.toolkit).map((tool) => ({ name: tool.name, description: tool.description })),
|
|
15211
|
+
...unloaded.length > 0 ? { unloaded, note: `Unloaded ${unloaded.join(", ")} to stay within the tool limit; load again if needed.` } : {}
|
|
15212
|
+
});
|
|
14919
15213
|
}
|
|
14920
15214
|
}];
|
|
14921
15215
|
}
|
|
14922
|
-
function
|
|
15216
|
+
function explainMissingTool(runtime, message) {
|
|
15217
|
+
if (!message.isError || !Array.isArray(message.content)) return;
|
|
15218
|
+
const first = message.content[0];
|
|
15219
|
+
const name = first?.type === "text" ? /^Tool (\S+) not found$/.exec(first.text ?? "")?.[1] : void 0;
|
|
15220
|
+
if (!name) return;
|
|
15221
|
+
let text;
|
|
15222
|
+
if (!runtime.progressive) {
|
|
15223
|
+
text = `${name} is not available in this conversation. Use only the tools listed for you.`;
|
|
15224
|
+
} else {
|
|
15225
|
+
const toolkit = metadata.get(name)?.tier;
|
|
15226
|
+
text = runtime.allowed.some((tool) => tool.name === name) && toolkit && toolkit !== "core" ? `${name} is not loaded yet. Call ${LOAD} with toolkit "${toolkit}", then call ${name} again.` : `${name} is not available in this conversation. Use ${DISCOVER} to see the tools you can load.`;
|
|
15227
|
+
}
|
|
15228
|
+
message.content = [{ type: "text", text }];
|
|
15229
|
+
}
|
|
15230
|
+
function configureAeroRuntime(agent, allowed, limits, progressive = true, pinned = []) {
|
|
14923
15231
|
let runtime = runtimes.get(agent);
|
|
14924
15232
|
if (!runtime) {
|
|
14925
|
-
runtime = { allowed, loaded: /* @__PURE__ */ new Set(), limits: agentTurnLimitsSchema.parse(limits ?? {}), calls: 0, rounds: 0, startedAt: 0, reason: "completed", progressive };
|
|
15233
|
+
runtime = { allowed, loaded: /* @__PURE__ */ new Set(), limits: agentTurnLimitsSchema.parse(limits ?? {}), calls: 0, rounds: 0, startedAt: 0, reason: "completed", progressive, pinned: new Set(pinned) };
|
|
14926
15234
|
runtimes.set(agent, runtime);
|
|
14927
15235
|
const state = runtime;
|
|
14928
15236
|
const stream = agent.streamFn;
|
|
@@ -14959,6 +15267,7 @@ function configureAeroRuntime(agent, allowed, limits, progressive = true) {
|
|
|
14959
15267
|
}
|
|
14960
15268
|
if (event.type === "message_end" && event.message.role === "toolResult") {
|
|
14961
15269
|
const message = event.message;
|
|
15270
|
+
explainMissingTool(state, message);
|
|
14962
15271
|
const startedAt = toolStarts.get(message.toolCallId);
|
|
14963
15272
|
Object.assign(message, {
|
|
14964
15273
|
aeroToolLabel: state.allowed.find((tool) => tool.name === message.toolName)?.label,
|
|
@@ -14990,6 +15299,7 @@ function configureAeroRuntime(agent, allowed, limits, progressive = true) {
|
|
|
14990
15299
|
runtime.loaded = /* @__PURE__ */ new Set();
|
|
14991
15300
|
runtime.limits = agentTurnLimitsSchema.parse(limits ?? {});
|
|
14992
15301
|
runtime.progressive = progressive;
|
|
15302
|
+
runtime.pinned = new Set(pinned);
|
|
14993
15303
|
agent.state.tools = visibleTools(runtime);
|
|
14994
15304
|
}
|
|
14995
15305
|
function aeroTurnStatus(agent) {
|
|
@@ -15036,7 +15346,7 @@ function buildAeroViewTool(options, validatedEvidence) {
|
|
|
15036
15346
|
}
|
|
15037
15347
|
};
|
|
15038
15348
|
}
|
|
15039
|
-
var AERO_RUNTIME_PROMPT = "\n\nNative Aero tools: when present, use aero_list_toolkits and aero_load_toolkit to discover and load task-specific tools before calling them. Otherwise use the exposed catalog. Start view-relative analysis with aero_inspect_view when available. Cite the returned source.href beside claims. Name the measurement date, class, numerator/denominator, and comparison limitations. Separate observations from possible explanations; do not infer causation. Never turn missing, incomplete, or truncated evidence into zero. Without current view context, resolve ambiguous references before making scoped claims.";
|
|
15349
|
+
var AERO_RUNTIME_PROMPT = "\n\nNative Aero tools: when present, use aero_list_toolkits and aero_load_toolkit to discover and load task-specific tools before calling them. Otherwise use the exposed catalog. Start view-relative analysis with aero_inspect_view when available. Cite the returned source.href beside claims. Name the measurement date, class, numerator/denominator, and comparison limitations. Separate observations from possible explanations; do not infer causation. Never turn missing, incomplete, or truncated evidence into zero. A tool result that says it was truncated is partial: never list, rank, count or group items you did not see; say what was cut. Without current view context, resolve ambiguous references before making scoped claims.";
|
|
15040
15350
|
function aeroViewPrompt(context) {
|
|
15041
15351
|
return context ? `
|
|
15042
15352
|
|
|
@@ -15460,6 +15770,59 @@ function writeCompactionNote(db, args) {
|
|
|
15460
15770
|
return inserted;
|
|
15461
15771
|
}
|
|
15462
15772
|
|
|
15773
|
+
// src/agent/project-shape.ts
|
|
15774
|
+
import { and as and17, count, eq as eq20 } from "drizzle-orm";
|
|
15775
|
+
var ADVANCED_TOOLS = [
|
|
15776
|
+
"canonry_measurement_portfolio_summary",
|
|
15777
|
+
"canonry_measurement_overview",
|
|
15778
|
+
"canonry_measurement_property_evidence",
|
|
15779
|
+
"canonry_measurement_property_competitors",
|
|
15780
|
+
"canonry_measurement_changes",
|
|
15781
|
+
"canonry_visibility_report",
|
|
15782
|
+
"canonry_analytics_sources"
|
|
15783
|
+
];
|
|
15784
|
+
var LEGACY_TOOLS = ["canonry_measurement_overview", "canonry_measurement_plan_get"];
|
|
15785
|
+
var SIMPLE_TOOLS = ["canonry_visibility_report"];
|
|
15786
|
+
function aeroProjectShape(db, projectId) {
|
|
15787
|
+
try {
|
|
15788
|
+
const pointer = db.select().from(measurementPlans).where(eq20(measurementPlans.projectId, projectId)).get();
|
|
15789
|
+
const version = pointer ? db.select().from(measurementPlanVersions).where(and17(
|
|
15790
|
+
eq20(measurementPlanVersions.projectId, projectId),
|
|
15791
|
+
eq20(measurementPlanVersions.id, pointer.activeVersionId)
|
|
15792
|
+
)).get() : void 0;
|
|
15793
|
+
if (version) {
|
|
15794
|
+
const plan = JSON.parse(version.canonicalJson);
|
|
15795
|
+
const groups = plan.groups ?? [];
|
|
15796
|
+
const topLevel = groups.filter((group) => !group.parentGroupKey).length;
|
|
15797
|
+
const size = `${plan.targets?.length ?? 0} Properties in ${groups.length} groups (${topLevel} top-level, ${groups.length - topLevel} nested)`;
|
|
15798
|
+
if ((plan.schemaVersion ?? version.schemaVersion) !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) {
|
|
15799
|
+
return {
|
|
15800
|
+
prompt: `
|
|
15801
|
+
|
|
15802
|
+
Project shape: an Advanced Measurement plan on the legacy schema v1 (plan revision ${version.revision}) with ${size}. Its queries are not classified as branded or non-brand, so do not report class splits for it. Read it with canonry_measurement_overview and canonry_measurement_plan_get; the portfolio ranking, Property evidence and the advanced visibility report need a schema-v2 plan and will refuse this one.`,
|
|
15803
|
+
pinned: LEGACY_TOOLS
|
|
15804
|
+
};
|
|
15805
|
+
}
|
|
15806
|
+
const queriesIn = (queryClass) => new Set((plan.assignments ?? []).filter((assignment) => assignment.queryClass === queryClass).map((assignment) => assignment.queryId)).size;
|
|
15807
|
+
return {
|
|
15808
|
+
prompt: `
|
|
15809
|
+
|
|
15810
|
+
Project shape: an Advanced Measurement portfolio (plan revision ${version.revision}) with ${size}, ${queriesIn("branded")} branded and ${queriesIn("non-brand")} non-brand queries. Measure per Property, group or market, never pool branded and non-brand, and a query shared by several Properties is one answer scored for each. Start portfolio questions with canonry_measurement_portfolio_summary (weakest-first ranking with each Property's metro, the names answers wrote instead, and the domains they cited) or canonry_measurement_overview; drill into one Property with canonry_measurement_property_evidence and canonry_measurement_property_competitors; compare sweeps with canonry_measurement_changes. Project-wide tools such as canonry_visibility_report describe the whole project, not one Property. Group Properties only by the metro and submarkets the tools return, never by name. Names given instead were written in the answer text, not cited. Denominators count answers (queries x engines), not queries. Properties tied at the weakest rate are not ranked against each other. For sources, use each Property's citedDomains in the portfolio summary, or canonry_analytics_sources with queryClass and runId set; without them it pools both classes and every sweep. canonry_measurement_plan_get is plan structure with no metrics; do not read it for analysis.`,
|
|
15811
|
+
pinned: ADVANCED_TOOLS
|
|
15812
|
+
};
|
|
15813
|
+
}
|
|
15814
|
+
const tracked = db.select({ total: count() }).from(queries).where(eq20(queries.projectId, projectId)).get()?.total ?? 0;
|
|
15815
|
+
return {
|
|
15816
|
+
prompt: `
|
|
15817
|
+
|
|
15818
|
+
Project shape: a Simple project with ${tracked} tracked queries and no measurement plan (no Properties or markets). Start with canonry_project_overview, then canonry_visibility_report for mention and citation rates by query and engine.`,
|
|
15819
|
+
pinned: SIMPLE_TOOLS
|
|
15820
|
+
};
|
|
15821
|
+
} catch {
|
|
15822
|
+
return { prompt: "", pinned: [] };
|
|
15823
|
+
}
|
|
15824
|
+
}
|
|
15825
|
+
|
|
15463
15826
|
// src/agent/compaction.ts
|
|
15464
15827
|
import { complete } from "@mariozechner/pi-ai";
|
|
15465
15828
|
|
|
@@ -15702,7 +16065,7 @@ var SessionRegistry = class {
|
|
|
15702
16065
|
modelProvider: effectiveProvider,
|
|
15703
16066
|
modelId: effectiveModelId,
|
|
15704
16067
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15705
|
-
}).where(
|
|
16068
|
+
}).where(eq21(agentSessions.projectId, projectId)).run();
|
|
15706
16069
|
}
|
|
15707
16070
|
const agent2 = createAeroSession({
|
|
15708
16071
|
projectName,
|
|
@@ -15869,9 +16232,10 @@ ${lines.join("\n")}
|
|
|
15869
16232
|
preferences?.signal?.throwIfAborted();
|
|
15870
16233
|
await this.maybeCompact(projectName, agent);
|
|
15871
16234
|
preferences?.signal?.throwIfAborted();
|
|
15872
|
-
agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, systemPrompt) + aeroViewPrompt(preferences?.context);
|
|
15873
16235
|
const progressive = (preferences?.toolProfile ?? AeroToolProfiles.default) === AeroToolProfiles.default;
|
|
15874
|
-
|
|
16236
|
+
const shape = aeroProjectShape(this.opts.db, projectId);
|
|
16237
|
+
agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, systemPrompt) + shape.prompt + aeroViewPrompt(preferences?.context);
|
|
16238
|
+
configureAeroRuntime(agent, [...agent.state.tools, ...progressive ? [buildAeroViewTool(view, evidence)] : []], preferences?.limits, progressive, shape.pinned);
|
|
15875
16239
|
return agent;
|
|
15876
16240
|
} finally {
|
|
15877
16241
|
this.acquisitions.delete(projectName);
|
|
@@ -15997,7 +16361,7 @@ ${lines.join("\n")}
|
|
|
15997
16361
|
modelProvider: nextProvider,
|
|
15998
16362
|
modelId: nextModelId,
|
|
15999
16363
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16000
|
-
}).where(
|
|
16364
|
+
}).where(eq21(agentSessions.projectId, projectId)).run();
|
|
16001
16365
|
}
|
|
16002
16366
|
/** Snapshot pending work without consuming it before an atomic conversation switch. */
|
|
16003
16367
|
prepareConversationChange(projectName) {
|
|
@@ -16171,11 +16535,11 @@ ${lines.join("\n")}
|
|
|
16171
16535
|
return id;
|
|
16172
16536
|
}
|
|
16173
16537
|
tryResolveProjectId(projectName) {
|
|
16174
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
16538
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq21(projects.name, projectName)).get();
|
|
16175
16539
|
return row?.id;
|
|
16176
16540
|
}
|
|
16177
16541
|
loadRow(projectId) {
|
|
16178
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
16542
|
+
const row = this.opts.db.select().from(agentSessions).where(eq21(agentSessions.projectId, projectId)).get();
|
|
16179
16543
|
return row ?? null;
|
|
16180
16544
|
}
|
|
16181
16545
|
insertRow(params) {
|
|
@@ -16194,7 +16558,7 @@ ${lines.join("\n")}
|
|
|
16194
16558
|
}
|
|
16195
16559
|
updateRow(projectId, patch) {
|
|
16196
16560
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16197
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
16561
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq21(agentSessions.projectId, projectId)).run();
|
|
16198
16562
|
}
|
|
16199
16563
|
/**
|
|
16200
16564
|
* Store a refreshed prompt snapshot without touching `updatedAt`, which the
|
|
@@ -16203,13 +16567,13 @@ ${lines.join("\n")}
|
|
|
16203
16567
|
* recently used right after an upgrade.
|
|
16204
16568
|
*/
|
|
16205
16569
|
persistPromptSnapshot(projectId, systemPrompt) {
|
|
16206
|
-
this.opts.db.update(agentSessions).set({ systemPrompt }).where(
|
|
16570
|
+
this.opts.db.update(agentSessions).set({ systemPrompt }).where(eq21(agentSessions.projectId, projectId)).run();
|
|
16207
16571
|
}
|
|
16208
16572
|
};
|
|
16209
16573
|
|
|
16210
16574
|
// src/agent/viewer-sessions.ts
|
|
16211
16575
|
import crypto25 from "crypto";
|
|
16212
|
-
import { and as
|
|
16576
|
+
import { and as and18, eq as eq22, isNotNull, like as like2 } from "drizzle-orm";
|
|
16213
16577
|
var AERO_VIEWER_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
|
|
16214
16578
|
CanonryMcpToolNames.canonry_ads_account,
|
|
16215
16579
|
CanonryMcpToolNames.canonry_ads_geo_search,
|
|
@@ -16239,7 +16603,6 @@ var AERO_VIEWER_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
|
|
|
16239
16603
|
var OPERATOR_ONLY_TOOL_NAMES = new Set(
|
|
16240
16604
|
canonryMcpTools.filter((tool) => tool.tier === "agent" || tool.requiresOperator).map((tool) => tool.name)
|
|
16241
16605
|
);
|
|
16242
|
-
var VIEWER_AERO_TURN_LIMITS = { maxToolCalls: 12, timeoutMs: 12e4 };
|
|
16243
16606
|
var VIEWER_AERO_MAX_PROMPT_CHARS = 4e3;
|
|
16244
16607
|
var VIEWER_AERO_DAILY_TURN_LIMIT = 50;
|
|
16245
16608
|
function viewerAeroDailyLimitExceeded(limit) {
|
|
@@ -16323,8 +16686,8 @@ var ViewerAeroSessions = class {
|
|
|
16323
16686
|
if (this.isBusy(project.name, userId)) throw agentBusy(project.name);
|
|
16324
16687
|
const today = new Date(this.now()).toISOString().slice(0, 10);
|
|
16325
16688
|
const used = this.turnsToday.get(key);
|
|
16326
|
-
const
|
|
16327
|
-
if (
|
|
16689
|
+
const count2 = used?.date === today ? used.count : 0;
|
|
16690
|
+
if (count2 >= VIEWER_AERO_DAILY_TURN_LIMIT) throw viewerAeroDailyLimitExceeded(VIEWER_AERO_DAILY_TURN_LIMIT);
|
|
16328
16691
|
const minted = mintViewerAeroKey(this.opts.db, userId, new Date(this.now()));
|
|
16329
16692
|
this.acquisitions.add(key);
|
|
16330
16693
|
let handedOff = false;
|
|
@@ -16335,10 +16698,11 @@ var ViewerAeroSessions = class {
|
|
|
16335
16698
|
const view = { client, projectName: project.name, basePath: this.opts.config.basePath, context: preferences.context };
|
|
16336
16699
|
const evidence = preferences.context ? await readAeroViewEvidence(view) : void 0;
|
|
16337
16700
|
preferences.signal?.throwIfAborted();
|
|
16338
|
-
|
|
16339
|
-
|
|
16701
|
+
const shape = aeroProjectShape(this.opts.db, project.id);
|
|
16702
|
+
agent.state.systemPrompt = loadAeroSystemPrompt(void 0, { extras: false }) + VIEWER_AERO_PROMPT + shape.prompt + aeroViewPrompt(preferences.context);
|
|
16703
|
+
configureAeroRuntime(agent, [...buildViewerAeroTools(client, project.name, this.opts.managedSweeps), buildAeroViewTool(view, evidence)], void 0, true, shape.pinned);
|
|
16340
16704
|
this.sessions.set(key, { agent, lastUsedAt: this.now(), updatedAt: this.sessions.get(key)?.updatedAt ?? null });
|
|
16341
|
-
this.turnsToday.set(key, { date: today, count:
|
|
16705
|
+
this.turnsToday.set(key, { date: today, count: count2 + 1 });
|
|
16342
16706
|
handedOff = true;
|
|
16343
16707
|
let released = false;
|
|
16344
16708
|
return {
|
|
@@ -16385,10 +16749,10 @@ var ViewerAeroSessions = class {
|
|
|
16385
16749
|
*/
|
|
16386
16750
|
removeKey(id) {
|
|
16387
16751
|
try {
|
|
16388
|
-
this.opts.db.delete(apiKeys).where(
|
|
16752
|
+
this.opts.db.delete(apiKeys).where(eq22(apiKeys.id, id)).run();
|
|
16389
16753
|
} catch {
|
|
16390
16754
|
try {
|
|
16391
|
-
this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(
|
|
16755
|
+
this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(eq22(apiKeys.id, id)).run();
|
|
16392
16756
|
} catch {
|
|
16393
16757
|
}
|
|
16394
16758
|
}
|
|
@@ -16400,7 +16764,7 @@ var ViewerAeroSessions = class {
|
|
|
16400
16764
|
*/
|
|
16401
16765
|
revokeOrphanedKeys() {
|
|
16402
16766
|
try {
|
|
16403
|
-
this.opts.db.delete(apiKeys).where(
|
|
16767
|
+
this.opts.db.delete(apiKeys).where(and18(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull(apiKeys.delegatedUserId))).run();
|
|
16404
16768
|
} catch {
|
|
16405
16769
|
}
|
|
16406
16770
|
}
|
|
@@ -16416,7 +16780,7 @@ var ViewerAeroSessions = class {
|
|
|
16416
16780
|
|
|
16417
16781
|
// src/mcp-http.ts
|
|
16418
16782
|
import crypto26 from "crypto";
|
|
16419
|
-
import { eq as
|
|
16783
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
16420
16784
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16421
16785
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
16422
16786
|
var SESSION_IDLE_MS = 10 * 60 * 1e3;
|
|
@@ -16437,7 +16801,7 @@ function mintSessionKey(db, scopes, userId) {
|
|
|
16437
16801
|
}
|
|
16438
16802
|
function revokeSessionKey(db, id) {
|
|
16439
16803
|
try {
|
|
16440
|
-
db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16804
|
+
db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(apiKeys.id, id)).run();
|
|
16441
16805
|
} catch {
|
|
16442
16806
|
}
|
|
16443
16807
|
}
|
|
@@ -16620,7 +16984,7 @@ function registerMcpHttpRoutes(scope, opts) {
|
|
|
16620
16984
|
}
|
|
16621
16985
|
|
|
16622
16986
|
// src/agent/agent-routes.ts
|
|
16623
|
-
import { eq as
|
|
16987
|
+
import { eq as eq24 } from "drizzle-orm";
|
|
16624
16988
|
function viewerAeroCaller(request, opts) {
|
|
16625
16989
|
if (!opts.viewerSessions) return null;
|
|
16626
16990
|
const principal = request.principal;
|
|
@@ -16650,7 +17014,7 @@ function viewerTranscript(opts, projectName, userId) {
|
|
|
16650
17014
|
};
|
|
16651
17015
|
}
|
|
16652
17016
|
function resolveProject(db, name) {
|
|
16653
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
17017
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq24(projects.name, name)).get();
|
|
16654
17018
|
if (!row) throw notFound("project", name);
|
|
16655
17019
|
return row;
|
|
16656
17020
|
}
|
|
@@ -16758,7 +17122,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16758
17122
|
}
|
|
16759
17123
|
requireInstanceAdministrator(request);
|
|
16760
17124
|
const project = resolveProject(opts.db, request.params.name);
|
|
16761
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
17125
|
+
const row = opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, project.id)).get();
|
|
16762
17126
|
if (!row) {
|
|
16763
17127
|
return { conversationId: null, messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
16764
17128
|
}
|
|
@@ -16798,7 +17162,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16798
17162
|
const project = resolveProject(opts.db, request.params.name);
|
|
16799
17163
|
if (opts.sessionRegistry.isBusy(project.name)) throw agentBusy(project.name);
|
|
16800
17164
|
opts.sessionRegistry.reset(project.name);
|
|
16801
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
17165
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(agentSessions.projectId, project.id)).run();
|
|
16802
17166
|
return { status: "reset" };
|
|
16803
17167
|
}
|
|
16804
17168
|
);
|
|
@@ -16822,7 +17186,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16822
17186
|
return streamViewerTurn(opts, project, viewerId, body, reply);
|
|
16823
17187
|
}
|
|
16824
17188
|
if (body.conversationId !== void 0) {
|
|
16825
|
-
const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(
|
|
17189
|
+
const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(eq24(agentSessions.projectId, project.id)).get();
|
|
16826
17190
|
if ((current?.id ?? null) !== body.conversationId) throw validationError("The active conversation changed. Reload the conversation before sending.");
|
|
16827
17191
|
}
|
|
16828
17192
|
const promptText = body.prompt;
|
|
@@ -17625,7 +17989,7 @@ function buildSnapshotSummary(companyName, queries2, providers, queryResults, au
|
|
|
17625
17989
|
competitorCounts.set(competitor, (competitorCounts.get(competitor) ?? 0) + 1);
|
|
17626
17990
|
}
|
|
17627
17991
|
}
|
|
17628
|
-
const topCompetitors = [...competitorCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10).map(([name,
|
|
17992
|
+
const topCompetitors = [...competitorCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10).map(([name, count2]) => ({ name, count: count2 }));
|
|
17629
17993
|
const defaultMeaning = totalComparisons > 0 ? `${companyName} was mentioned in ${mentionCount}/${totalComparisons} successful provider-query response${totalComparisons === 1 ? "" : "s"} across ${queries2.length} category queries.` : `No successful provider responses were returned across ${queries2.length} category queries.`;
|
|
17630
17994
|
const failureNote = failedComparisons > 0 ? [
|
|
17631
17995
|
`${failedComparisons} provider response${failedComparisons === 1 ? "" : "s"} failed and ${failedComparisons === 1 ? "was" : "were"} excluded from visibility totals.`
|
|
@@ -17760,20 +18124,20 @@ function clipText(value, length) {
|
|
|
17760
18124
|
}
|
|
17761
18125
|
|
|
17762
18126
|
// src/research-runner.ts
|
|
17763
|
-
import { and as
|
|
18127
|
+
import { and as and19, eq as eq25, inArray as inArray9, sql as sql9 } from "drizzle-orm";
|
|
17764
18128
|
var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
|
|
17765
18129
|
function finalResearchRunStatus(completed, failed) {
|
|
17766
18130
|
if (failed === 0) return ResearchRunStatuses.completed;
|
|
17767
18131
|
return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
|
|
17768
18132
|
}
|
|
17769
18133
|
async function executeResearchRun(db, registry, runId, projectId) {
|
|
17770
|
-
const run = db.select().from(researchRuns).where(
|
|
18134
|
+
const run = db.select().from(researchRuns).where(and19(eq25(researchRuns.id, runId), eq25(researchRuns.projectId, projectId))).get();
|
|
17771
18135
|
if (!run || run.status !== ResearchRunStatuses.queued) return;
|
|
17772
|
-
const project = db.select().from(projects).where(
|
|
18136
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
17773
18137
|
if (!project) return;
|
|
17774
18138
|
const provider = registry.get(run.provider);
|
|
17775
18139
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17776
|
-
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(
|
|
18140
|
+
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and19(eq25(researchRuns.id, runId), eq25(researchRuns.status, ResearchRunStatuses.queued))).run();
|
|
17777
18141
|
if (claim.changes !== 1) return;
|
|
17778
18142
|
let reserved = 0;
|
|
17779
18143
|
let dispatched = 0;
|
|
@@ -17797,8 +18161,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
17797
18161
|
}
|
|
17798
18162
|
reserved = run.totalQueries;
|
|
17799
18163
|
reservation = { scope, period };
|
|
17800
|
-
const rows = db.select().from(researchRunQueries).where(
|
|
17801
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
18164
|
+
const rows = db.select().from(researchRunQueries).where(eq25(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
|
|
18165
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq25(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
17802
18166
|
const domains = effectiveDomains(project);
|
|
17803
18167
|
const brands = effectiveBrandNames(project);
|
|
17804
18168
|
const config = { ...provider.config, model: run.resolvedModel };
|
|
@@ -17806,7 +18170,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
17806
18170
|
await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
|
|
17807
18171
|
try {
|
|
17808
18172
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17809
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(
|
|
18173
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and19(eq25(researchRunQueries.id, row.id), eq25(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
|
|
17810
18174
|
const raw = await gate.run(async () => {
|
|
17811
18175
|
dispatched++;
|
|
17812
18176
|
return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
|
|
@@ -17833,7 +18197,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
17833
18197
|
citationState: determineCitationState(normalized, domains),
|
|
17834
18198
|
rawResponse: raw.rawResponse,
|
|
17835
18199
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17836
|
-
}).where(
|
|
18200
|
+
}).where(and19(eq25(researchRunQueries.id, row.id), eq25(researchRunQueries.status, ResearchQueryStatuses.running))).run();
|
|
17837
18201
|
if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
|
|
17838
18202
|
} catch (error) {
|
|
17839
18203
|
try {
|
|
@@ -17857,18 +18221,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
17857
18221
|
}
|
|
17858
18222
|
}
|
|
17859
18223
|
function incrementResearchProgress(db, runId, column) {
|
|
17860
|
-
db.update(researchRuns).set({ [column]: sql9`${researchRuns[column]} + 1` }).where(
|
|
18224
|
+
db.update(researchRuns).set({ [column]: sql9`${researchRuns[column]} + 1` }).where(eq25(researchRuns.id, runId)).run();
|
|
17861
18225
|
}
|
|
17862
18226
|
function markResearchQueryFailed(db, runId, queryId, error) {
|
|
17863
18227
|
const message = describeError(error);
|
|
17864
|
-
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18228
|
+
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and19(eq25(researchRunQueries.id, queryId), inArray9(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
17865
18229
|
if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
|
|
17866
18230
|
}
|
|
17867
18231
|
function markUnfinishedResearchQueriesFailed(db, runId, error) {
|
|
17868
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18232
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and19(eq25(researchRunQueries.researchRunId, runId), inArray9(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
17869
18233
|
}
|
|
17870
18234
|
function finalizeResearchRun(db, runId, fatalError) {
|
|
17871
|
-
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(
|
|
18235
|
+
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq25(researchRunQueries.researchRunId, runId)).all();
|
|
17872
18236
|
const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
|
|
17873
18237
|
const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
|
|
17874
18238
|
db.update(researchRuns).set({
|
|
@@ -17877,11 +18241,11 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
17877
18241
|
failedQueries: failed,
|
|
17878
18242
|
error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
|
|
17879
18243
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17880
|
-
}).where(
|
|
18244
|
+
}).where(eq25(researchRuns.id, runId)).run();
|
|
17881
18245
|
}
|
|
17882
18246
|
|
|
17883
18247
|
// src/google-marketing-sync.ts
|
|
17884
|
-
import { and as
|
|
18248
|
+
import { and as and20, eq as eq26, isNull as isNull2 } from "drizzle-orm";
|
|
17885
18249
|
function safeErrorMessage(error) {
|
|
17886
18250
|
const raw = describeError(error);
|
|
17887
18251
|
return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
|
|
@@ -17904,10 +18268,10 @@ function assertSnapshotContext(metadata2, expected) {
|
|
|
17904
18268
|
}
|
|
17905
18269
|
}
|
|
17906
18270
|
function requireSyncRun(db, runId, projectId, kind) {
|
|
17907
|
-
const run = db.select().from(runs).where(
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
18271
|
+
const run = db.select().from(runs).where(and20(
|
|
18272
|
+
eq26(runs.id, runId),
|
|
18273
|
+
eq26(runs.projectId, projectId),
|
|
18274
|
+
eq26(runs.kind, kind)
|
|
17911
18275
|
)).get();
|
|
17912
18276
|
if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
|
|
17913
18277
|
if (run.status !== RunStatuses.queued) {
|
|
@@ -17920,19 +18284,19 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
|
|
|
17920
18284
|
status: RunStatuses.failed,
|
|
17921
18285
|
error: serializeRunError({ message: safeErrorMessage(error) }),
|
|
17922
18286
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17923
|
-
}).where(
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
18287
|
+
}).where(and20(
|
|
18288
|
+
eq26(runs.id, runId),
|
|
18289
|
+
eq26(runs.projectId, projectId),
|
|
18290
|
+
eq26(runs.kind, kind),
|
|
18291
|
+
eq26(runs.status, expectedStatus)
|
|
17928
18292
|
)).run();
|
|
17929
18293
|
}
|
|
17930
18294
|
function claimSyncRun(db, runId, projectId, kind, startedAt) {
|
|
17931
|
-
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
18295
|
+
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and20(
|
|
18296
|
+
eq26(runs.id, runId),
|
|
18297
|
+
eq26(runs.projectId, projectId),
|
|
18298
|
+
eq26(runs.kind, kind),
|
|
18299
|
+
eq26(runs.status, RunStatuses.queued)
|
|
17936
18300
|
)).run();
|
|
17937
18301
|
return claim.changes === 1;
|
|
17938
18302
|
}
|
|
@@ -17940,9 +18304,9 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
17940
18304
|
let claimed = false;
|
|
17941
18305
|
try {
|
|
17942
18306
|
requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
|
|
17943
|
-
const project = db.select().from(projects).where(
|
|
18307
|
+
const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
|
|
17944
18308
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
17945
|
-
const connection = db.select().from(googleAdsConnections).where(
|
|
18309
|
+
const connection = db.select().from(googleAdsConnections).where(eq26(googleAdsConnections.projectId, projectId)).get();
|
|
17946
18310
|
if (!connection?.selectedCustomerId) {
|
|
17947
18311
|
throw new Error("Google Ads needs a selected customer before it can sync.");
|
|
17948
18312
|
}
|
|
@@ -17993,11 +18357,11 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
17993
18357
|
status: RunStatuses.completed,
|
|
17994
18358
|
finishedAt,
|
|
17995
18359
|
error: null
|
|
17996
|
-
}).where(
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18360
|
+
}).where(and20(
|
|
18361
|
+
eq26(runs.id, runId),
|
|
18362
|
+
eq26(runs.projectId, projectId),
|
|
18363
|
+
eq26(runs.kind, RunKinds["google-ads-sync"]),
|
|
18364
|
+
eq26(runs.status, RunStatuses.running)
|
|
18001
18365
|
)).run();
|
|
18002
18366
|
if (terminalClaim.changes !== 1) {
|
|
18003
18367
|
throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
|
|
@@ -18014,12 +18378,12 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
18014
18378
|
lastMetricsSnapshotAt: result2.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
|
|
18015
18379
|
lastMetricsSnapshotId: result2.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
|
|
18016
18380
|
updatedAt: finishedAt
|
|
18017
|
-
}).where(
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
|
|
18021
|
-
connection.selectedLoginCustomerId === null ? isNull2(googleAdsConnections.selectedLoginCustomerId) :
|
|
18022
|
-
|
|
18381
|
+
}).where(and20(
|
|
18382
|
+
eq26(googleAdsConnections.id, connection.id),
|
|
18383
|
+
eq26(googleAdsConnections.projectId, projectId),
|
|
18384
|
+
eq26(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
|
|
18385
|
+
connection.selectedLoginCustomerId === null ? isNull2(googleAdsConnections.selectedLoginCustomerId) : eq26(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
|
|
18386
|
+
eq26(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
|
|
18023
18387
|
)).run();
|
|
18024
18388
|
if (connectionClaim.changes !== 1) {
|
|
18025
18389
|
throw new Error("Google Ads selection changed while its evidence was being collected.");
|
|
@@ -18046,9 +18410,9 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
18046
18410
|
let claimed = false;
|
|
18047
18411
|
try {
|
|
18048
18412
|
requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
|
|
18049
|
-
const project = db.select().from(projects).where(
|
|
18413
|
+
const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
|
|
18050
18414
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
18051
|
-
const connection = db.select().from(gtmConnections).where(
|
|
18415
|
+
const connection = db.select().from(gtmConnections).where(eq26(gtmConnections.projectId, projectId)).get();
|
|
18052
18416
|
if (!connection?.selectedAccountId || !connection.selectedContainerId) {
|
|
18053
18417
|
throw new Error("GTM needs a selected account and container before it can sync.");
|
|
18054
18418
|
}
|
|
@@ -18083,11 +18447,11 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
18083
18447
|
status: RunStatuses.completed,
|
|
18084
18448
|
finishedAt,
|
|
18085
18449
|
error: null
|
|
18086
|
-
}).where(
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18450
|
+
}).where(and20(
|
|
18451
|
+
eq26(runs.id, runId),
|
|
18452
|
+
eq26(runs.projectId, projectId),
|
|
18453
|
+
eq26(runs.kind, RunKinds["gtm-sync"]),
|
|
18454
|
+
eq26(runs.status, RunStatuses.running)
|
|
18091
18455
|
)).run();
|
|
18092
18456
|
if (terminalClaim.changes !== 1) {
|
|
18093
18457
|
throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
|
|
@@ -18101,13 +18465,13 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
18101
18465
|
lastSnapshotAt: snapshot.metadata.capturedAt,
|
|
18102
18466
|
lastSnapshotId: snapshot.metadata.id,
|
|
18103
18467
|
updatedAt: finishedAt
|
|
18104
|
-
}).where(
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
connection.selectedWorkspaceId === null ? isNull2(gtmConnections.selectedWorkspaceId) :
|
|
18110
|
-
|
|
18468
|
+
}).where(and20(
|
|
18469
|
+
eq26(gtmConnections.id, connection.id),
|
|
18470
|
+
eq26(gtmConnections.projectId, projectId),
|
|
18471
|
+
eq26(gtmConnections.selectedAccountId, connection.selectedAccountId),
|
|
18472
|
+
eq26(gtmConnections.selectedContainerId, connection.selectedContainerId),
|
|
18473
|
+
connection.selectedWorkspaceId === null ? isNull2(gtmConnections.selectedWorkspaceId) : eq26(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
|
|
18474
|
+
eq26(gtmConnections.selectionGeneration, connection.selectionGeneration)
|
|
18111
18475
|
)).run();
|
|
18112
18476
|
if (connectionClaim.changes !== 1) {
|
|
18113
18477
|
throw new Error("GTM selection changed while its evidence was being collected.");
|
|
@@ -18611,7 +18975,7 @@ async function createServer(opts) {
|
|
|
18611
18975
|
async (ctx) => {
|
|
18612
18976
|
if (!sessionRegistry) return;
|
|
18613
18977
|
if (!agentProactive) return;
|
|
18614
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
18978
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq27(projects.id, ctx.projectId)).get();
|
|
18615
18979
|
if (!project) return;
|
|
18616
18980
|
let content;
|
|
18617
18981
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -19068,7 +19432,7 @@ async function createServer(opts) {
|
|
|
19068
19432
|
void (async () => {
|
|
19069
19433
|
try {
|
|
19070
19434
|
const report = await schedulerClient.runDoctor({ project: projectName });
|
|
19071
|
-
const project = opts.db.select().from(projects).where(
|
|
19435
|
+
const project = opts.db.select().from(projects).where(eq27(projects.name, projectName)).get();
|
|
19072
19436
|
if (!project) {
|
|
19073
19437
|
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
19074
19438
|
return;
|
|
@@ -19096,9 +19460,9 @@ async function createServer(opts) {
|
|
|
19096
19460
|
});
|
|
19097
19461
|
if (!probed) return;
|
|
19098
19462
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
19099
|
-
|
|
19100
|
-
|
|
19101
|
-
|
|
19463
|
+
and21(
|
|
19464
|
+
eq27(ccReleaseSyncs.release, probed.release),
|
|
19465
|
+
eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
19102
19466
|
)
|
|
19103
19467
|
).limit(1).get();
|
|
19104
19468
|
if (alreadySynced) {
|
|
@@ -19369,7 +19733,7 @@ async function createServer(opts) {
|
|
|
19369
19733
|
})();
|
|
19370
19734
|
if (opts.config.apiKey) {
|
|
19371
19735
|
const keyHash = hashApiKey2(opts.config.apiKey);
|
|
19372
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
19736
|
+
const existing = opts.db.select().from(apiKeys).where(eq27(apiKeys.keyHash, keyHash)).get();
|
|
19373
19737
|
if (!existing) {
|
|
19374
19738
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
19375
19739
|
opts.db.insert(apiKeys).values({
|
|
@@ -19421,7 +19785,7 @@ async function createServer(opts) {
|
|
|
19421
19785
|
};
|
|
19422
19786
|
const getDefaultApiKey = () => {
|
|
19423
19787
|
if (!opts.config.apiKey) return void 0;
|
|
19424
|
-
return opts.db.select().from(apiKeys).where(
|
|
19788
|
+
return opts.db.select().from(apiKeys).where(eq27(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
|
|
19425
19789
|
};
|
|
19426
19790
|
const createPasswordSession = (reply) => {
|
|
19427
19791
|
const key = getDefaultApiKey();
|
|
@@ -19445,7 +19809,7 @@ async function createServer(opts) {
|
|
|
19445
19809
|
if (!header) return false;
|
|
19446
19810
|
const parts = header.split(" ");
|
|
19447
19811
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
19448
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
19812
|
+
const key = opts.db.select().from(apiKeys).where(eq27(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
|
|
19449
19813
|
return Boolean(key && !key.revokedAt);
|
|
19450
19814
|
};
|
|
19451
19815
|
const namedAccountsInUse = () => anyUsersExist(opts.db);
|
|
@@ -19535,12 +19899,12 @@ async function createServer(opts) {
|
|
|
19535
19899
|
return reply.send({ authenticated: true });
|
|
19536
19900
|
}
|
|
19537
19901
|
if (apiKey) {
|
|
19538
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
19902
|
+
const key = opts.db.select().from(apiKeys).where(eq27(apiKeys.keyHash, hashApiKey2(apiKey))).get();
|
|
19539
19903
|
if (!key || key.revokedAt) {
|
|
19540
19904
|
const err2 = authInvalid();
|
|
19541
19905
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
19542
19906
|
}
|
|
19543
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
19907
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(apiKeys.id, key.id)).run();
|
|
19544
19908
|
const sessionId = createSession(key.id);
|
|
19545
19909
|
reply.header(
|
|
19546
19910
|
"set-cookie",
|
|
@@ -19703,8 +20067,8 @@ async function createServer(opts) {
|
|
|
19703
20067
|
getAgentPin: () => describeAgentPin(opts.config),
|
|
19704
20068
|
getGoogleMarketingDoctorInput: (ctx) => {
|
|
19705
20069
|
if (!ctx.project) return null;
|
|
19706
|
-
const googleAds = opts.db.select().from(googleAdsConnections).where(
|
|
19707
|
-
const gtm = opts.db.select().from(gtmConnections).where(
|
|
20070
|
+
const googleAds = opts.db.select().from(googleAdsConnections).where(eq27(googleAdsConnections.projectId, ctx.project.id)).get();
|
|
20071
|
+
const gtm = opts.db.select().from(gtmConnections).where(eq27(gtmConnections.projectId, ctx.project.id)).get();
|
|
19708
20072
|
const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
|
|
19709
20073
|
const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
|
|
19710
20074
|
const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
|
|
@@ -19955,7 +20319,7 @@ async function createServer(opts) {
|
|
|
19955
20319
|
...inspectOpts,
|
|
19956
20320
|
config: opts.config
|
|
19957
20321
|
}).then(() => {
|
|
19958
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
20322
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq27(runs.id, runId)).get();
|
|
19959
20323
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
19960
20324
|
return maybeRefreshGscCoverage(
|
|
19961
20325
|
opts.db,
|
|
@@ -20053,7 +20417,7 @@ async function createServer(opts) {
|
|
|
20053
20417
|
const entry = providerSummary.find((p) => p.name === name);
|
|
20054
20418
|
if (entry) {
|
|
20055
20419
|
entry.configured = true;
|
|
20056
|
-
entry.model =
|
|
20420
|
+
entry.model = registry.get(name)?.config.model;
|
|
20057
20421
|
entry.quota = quota;
|
|
20058
20422
|
if (name === "gemini") {
|
|
20059
20423
|
entry.vertexConfigured = !!opts.config.providers?.[name]?.vertexProject;
|
|
@@ -20221,7 +20585,7 @@ async function createServer(opts) {
|
|
|
20221
20585
|
}
|
|
20222
20586
|
];
|
|
20223
20587
|
},
|
|
20224
|
-
onGenerateQueries: async (providerName,
|
|
20588
|
+
onGenerateQueries: async (providerName, count2, project) => {
|
|
20225
20589
|
const provider = registry.get(providerName);
|
|
20226
20590
|
if (!provider)
|
|
20227
20591
|
throw new Error(`Provider "${providerName}" is not configured`);
|
|
@@ -20233,10 +20597,10 @@ async function createServer(opts) {
|
|
|
20233
20597
|
language: project.language,
|
|
20234
20598
|
existingQueries: project.existingQueries,
|
|
20235
20599
|
siteText,
|
|
20236
|
-
count
|
|
20600
|
+
count: count2
|
|
20237
20601
|
});
|
|
20238
20602
|
const raw = await provider.adapter.generateText(prompt, provider.config);
|
|
20239
|
-
return parseQueryResponse(raw,
|
|
20603
|
+
return parseQueryResponse(raw, count2);
|
|
20240
20604
|
},
|
|
20241
20605
|
onSnapshotRequested: async (input) => {
|
|
20242
20606
|
return snapshotService.createReport(input);
|
|
@@ -20431,7 +20795,7 @@ async function createServer(opts) {
|
|
|
20431
20795
|
},
|
|
20432
20796
|
notify: (projectId, result2) => notifier.onSiteLivenessChecked(projectId, result2)
|
|
20433
20797
|
});
|
|
20434
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
20798
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq27(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
20435
20799
|
dispatchResearchRun(run.id, run.projectId);
|
|
20436
20800
|
}
|
|
20437
20801
|
runtimeStartupSettled = true;
|
|
@@ -20486,7 +20850,7 @@ function buildQueryGenerationPrompt(ctx) {
|
|
|
20486
20850
|
);
|
|
20487
20851
|
return lines.join("\n");
|
|
20488
20852
|
}
|
|
20489
|
-
function parseQueryResponse(raw,
|
|
20853
|
+
function parseQueryResponse(raw, count2) {
|
|
20490
20854
|
const seen = /* @__PURE__ */ new Set();
|
|
20491
20855
|
const results = [];
|
|
20492
20856
|
for (const line of raw.split("\n")) {
|
|
@@ -20502,7 +20866,7 @@ function parseQueryResponse(raw, count) {
|
|
|
20502
20866
|
if (seen.has(key)) continue;
|
|
20503
20867
|
seen.add(key);
|
|
20504
20868
|
results.push(cleaned);
|
|
20505
|
-
if (results.length >=
|
|
20869
|
+
if (results.length >= count2) break;
|
|
20506
20870
|
}
|
|
20507
20871
|
return results;
|
|
20508
20872
|
}
|