@canonry/canonry 5.12.0 → 5.14.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/canonry/references/canonry-cli.md +5 -0
- package/assets/assets/{AuditHistoryPanel-DL_NSpfm.js → AuditHistoryPanel-DWDOPf8c.js} +1 -1
- package/assets/assets/{BacklinksPage-BtcBybtX.js → BacklinksPage-DGxq-iU6.js} +1 -1
- package/assets/assets/{HistoryPage-jXj-qi9c.js → HistoryPage-DR91m-gT.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-DKw401ok.js → MeasurementPropertyPage-a8poltfM.js} +1 -1
- package/assets/assets/{ProjectPage-og4qfFFD.js → ProjectPage-CCCJE7rA.js} +1 -1
- package/assets/assets/{RunRow-nK5iaZe1.js → RunRow-Xe63DyPK.js} +1 -1
- package/assets/assets/{RunsPage-CMgr3YSF.js → RunsPage-DK119ZZw.js} +1 -1
- package/assets/assets/{SettingsPage-QtElCcew.js → SettingsPage-G461foD-.js} +1 -1
- package/assets/assets/{SiteHealthSection-DNdVVl30.js → SiteHealthSection-CJnHkGM2.js} +3 -3
- package/assets/assets/{TrafficPage-C0WgvfRj.js → TrafficPage-AsWgc7Ot.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-jOTedQtt.js → TrafficSourceDetailPage-D10yBQoD.js} +1 -1
- package/assets/assets/{extract-error-message-DO_053o0.js → extract-error-message-PP4HD_Dp.js} +1 -1
- package/assets/assets/{index-CnVwWE3V.js → index-CCIeAE4J.js} +22 -22
- package/assets/assets/{react-sigma_core.esm.min-DJttq2UK.js → react-sigma_core.esm.min-CkyyYkKU.js} +1 -1
- package/assets/assets/{v2-overview-adapter-BC_oa__K.js → v2-overview-adapter-DW-In1hp.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-WJGUBZSW.js → chunk-2W6GCJ3O.js} +65 -1
- package/dist/{chunk-NYRRWWEE.js → chunk-45EGDRVF.js} +3043 -2923
- package/dist/{chunk-LWGYRN3T.js → chunk-4LPV6ZZY.js} +2 -2
- package/dist/{chunk-DOC4QQLS.js → chunk-RGMUT5KY.js} +2549 -2099
- package/dist/{chunk-KBSAEALE.js → chunk-S6LJSUZO.js} +424 -161
- package/dist/{chunk-S4BIBUCK.js → chunk-SOLFOKZF.js} +2 -2
- package/dist/cli.js +133 -7
- package/dist/{demo-server-OTXS6ZHS.js → demo-server-H4ZFIIIB.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/{intelligence-service-K4U55A2J.js → intelligence-service-CJKOXPOP.js} +2 -2
- package/dist/mcp.js +3 -3
- package/package.json +10 -10
|
@@ -17,12 +17,12 @@ import {
|
|
|
17
17
|
resolveOperatorApiKeyIds,
|
|
18
18
|
runWithUsageTags,
|
|
19
19
|
saveConfigPatch
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2W6GCJ3O.js";
|
|
21
21
|
import {
|
|
22
22
|
deleteSiteCrawlGraphLayout,
|
|
23
23
|
persistSiteCrawlGraphLayout,
|
|
24
24
|
prepareSiteCrawlGraphLayout
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-4LPV6ZZY.js";
|
|
26
26
|
import {
|
|
27
27
|
PACKAGE_VERSION
|
|
28
28
|
} from "./chunk-AOJU5LKX.js";
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
createCredentialChecker,
|
|
74
74
|
createFastifyLogger,
|
|
75
75
|
createLogger,
|
|
76
|
+
createRunCompetitorResolver,
|
|
76
77
|
createUserSession,
|
|
77
78
|
deliverWebhook,
|
|
78
79
|
determineCitationState,
|
|
@@ -134,7 +135,10 @@ import {
|
|
|
134
135
|
loadDuckdb,
|
|
135
136
|
markSessionFailed,
|
|
136
137
|
measurementRunCompleteness,
|
|
138
|
+
measurementRunSlotState,
|
|
139
|
+
measurementSlotKey,
|
|
137
140
|
migrate,
|
|
141
|
+
newerFullSweep,
|
|
138
142
|
nextRunFromCron,
|
|
139
143
|
nextRunFromSchedule,
|
|
140
144
|
notifications,
|
|
@@ -164,6 +168,7 @@ import {
|
|
|
164
168
|
resolveVercelSyncDeadlineMs,
|
|
165
169
|
resolveWebhookTarget,
|
|
166
170
|
runChecks,
|
|
171
|
+
runFills,
|
|
167
172
|
runs,
|
|
168
173
|
schedules,
|
|
169
174
|
serializeUserSessionCookie,
|
|
@@ -179,7 +184,7 @@ import {
|
|
|
179
184
|
siteLivenessState,
|
|
180
185
|
toAlertView,
|
|
181
186
|
usageCounters
|
|
182
|
-
} from "./chunk-
|
|
187
|
+
} from "./chunk-RGMUT5KY.js";
|
|
183
188
|
import {
|
|
184
189
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
185
190
|
AGENT_NONE,
|
|
@@ -211,6 +216,7 @@ import {
|
|
|
211
216
|
ONBOARDING_FLOW_VERSION,
|
|
212
217
|
ProviderNames,
|
|
213
218
|
ROUTE_CAPABLE_CITED_URL_PROVIDERS,
|
|
219
|
+
RUN_FILL_PROVIDER_BREAKER,
|
|
214
220
|
ResearchQueryStatuses,
|
|
215
221
|
ResearchRunStatuses,
|
|
216
222
|
RunKinds,
|
|
@@ -272,6 +278,7 @@ import {
|
|
|
272
278
|
filterCapturedCitedUrls,
|
|
273
279
|
forbidden,
|
|
274
280
|
formatIsoDateInTimeZone,
|
|
281
|
+
formatRunErrorOneLine,
|
|
275
282
|
frameAncestorsHeaderValue,
|
|
276
283
|
googleAdsAccessibleCustomerDtoSchema,
|
|
277
284
|
googleAdsAccessibleCustomersResponseSchema,
|
|
@@ -313,6 +320,7 @@ import {
|
|
|
313
320
|
observeTemplateLinkEdges,
|
|
314
321
|
parseMeasurementRunManifestV1,
|
|
315
322
|
parseOriginList,
|
|
323
|
+
parseRunError,
|
|
316
324
|
providerError,
|
|
317
325
|
providerSupportsLocationContext,
|
|
318
326
|
registrableDomain,
|
|
@@ -337,7 +345,7 @@ import {
|
|
|
337
345
|
visibilityReportRequestSchema,
|
|
338
346
|
winnabilityClassLabel,
|
|
339
347
|
withRetry
|
|
340
|
-
} from "./chunk-
|
|
348
|
+
} from "./chunk-45EGDRVF.js";
|
|
341
349
|
|
|
342
350
|
// src/runtime-context.ts
|
|
343
351
|
function cliRuntimeContext(env = process.env, stdio = process) {
|
|
@@ -6483,7 +6491,7 @@ function nonBlank(value) {
|
|
|
6483
6491
|
return trimmed ? trimmed : void 0;
|
|
6484
6492
|
}
|
|
6485
6493
|
function resolveBuildCommit(env = process.env) {
|
|
6486
|
-
const embedded = true ? "
|
|
6494
|
+
const embedded = true ? "f4648ef75d44ebb5a3396cf05739a56a4aa2b7ee" : void 0;
|
|
6487
6495
|
return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
|
|
6488
6496
|
}
|
|
6489
6497
|
function resolveInstanceIdentity(env = process.env) {
|
|
@@ -6831,6 +6839,8 @@ var JobRunner = class {
|
|
|
6831
6839
|
this.registry = registry;
|
|
6832
6840
|
}
|
|
6833
6841
|
recoverStaleRuns() {
|
|
6842
|
+
const staleFills = this.db.update(runFills).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: "Server restarted while the fill was in progress" }).where(inArray(runFills.status, ["queued", "running"])).run();
|
|
6843
|
+
if (staleFills.changes > 0) log2.warn("fill.recovered-stale", { count: staleFills.changes });
|
|
6834
6844
|
const stale = this.db.select({
|
|
6835
6845
|
id: runs.id,
|
|
6836
6846
|
projectId: runs.projectId,
|
|
@@ -6956,7 +6966,8 @@ var JobRunner = class {
|
|
|
6956
6966
|
}
|
|
6957
6967
|
log2.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
6958
6968
|
const projectCompetitors = this.db.select().from(competitors).where(eq2(competitors.projectId, projectId)).all();
|
|
6959
|
-
const
|
|
6969
|
+
const runCompetitors = createRunCompetitorResolver(this.db, projectCompetitors.map((c) => c.domain))(existingRun.measurementPlanVersionId);
|
|
6970
|
+
const competitorDomains = runCompetitors.domains;
|
|
6960
6971
|
const allDomains = effectiveDomains({
|
|
6961
6972
|
canonicalDomain: project.canonicalDomain,
|
|
6962
6973
|
ownedDomains: project.ownedDomains
|
|
@@ -7156,146 +7167,20 @@ var JobRunner = class {
|
|
|
7156
7167
|
}
|
|
7157
7168
|
}
|
|
7158
7169
|
};
|
|
7159
|
-
const
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
await gate.run(async () => {
|
|
7171
|
-
this.throwIfRunCancelled(runId);
|
|
7172
|
-
providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
7173
|
-
const raw = await adapter.executeTrackedQuery(
|
|
7174
|
-
{
|
|
7175
|
-
query: unit.queryText,
|
|
7176
|
-
canonicalDomains: allDomains,
|
|
7177
|
-
competitorDomains,
|
|
7178
|
-
location: requestedContext ?? void 0
|
|
7179
|
-
},
|
|
7180
|
-
config
|
|
7181
|
-
);
|
|
7182
|
-
this.throwIfRunCancelled(runId);
|
|
7183
|
-
const providerResult = adapter.normalizeResult(raw);
|
|
7184
|
-
const rawGroundingSources = providerResult.groundingSources;
|
|
7185
|
-
const normalized = {
|
|
7186
|
-
...providerResult,
|
|
7187
|
-
groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
|
|
7188
|
-
};
|
|
7189
|
-
let citedUrlCapture;
|
|
7190
|
-
try {
|
|
7191
|
-
citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
|
|
7192
|
-
} catch (err) {
|
|
7193
|
-
citedUrlCapture = {
|
|
7194
|
-
citedUrls: [],
|
|
7195
|
-
captureStatus: "failed",
|
|
7196
|
-
sourceCount: normalized.groundingSources.length,
|
|
7197
|
-
resolvedCount: 0,
|
|
7198
|
-
captureVersion: CITED_URL_CAPTURE_VERSION
|
|
7199
|
-
};
|
|
7200
|
-
log2.warn("query.cited-url-capture-failed", {
|
|
7201
|
-
runId,
|
|
7202
|
-
provider: providerName,
|
|
7203
|
-
query: unit.queryText,
|
|
7204
|
-
error: describeError(err)
|
|
7205
|
-
});
|
|
7206
|
-
}
|
|
7207
|
-
this.throwIfRunCancelled(runId);
|
|
7208
|
-
const citationState = determineCitationState(normalized, allDomains);
|
|
7209
|
-
const answerMentioned = determineAnswerMentioned(
|
|
7210
|
-
normalized.answerText,
|
|
7211
|
-
allBrandNames,
|
|
7212
|
-
allDomains
|
|
7213
|
-
);
|
|
7214
|
-
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
7215
|
-
const extractedCompetitors = extractRecommendedCompetitors(
|
|
7216
|
-
normalized.answerText,
|
|
7217
|
-
allDomains,
|
|
7218
|
-
normalized.citedDomains,
|
|
7219
|
-
competitorDomains,
|
|
7220
|
-
allBrandNames
|
|
7221
|
-
);
|
|
7222
|
-
const snapshotId = crypto6.randomUUID();
|
|
7223
|
-
let screenshotRelPath = null;
|
|
7224
|
-
if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
|
|
7225
|
-
const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
|
|
7226
|
-
if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
|
|
7227
|
-
const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
|
|
7228
|
-
fs3.renameSync(raw.screenshotPath, destPath);
|
|
7229
|
-
screenshotRelPath = `${runId}/${snapshotId}.png`;
|
|
7230
|
-
}
|
|
7231
|
-
this.db.insert(querySnapshots).values({
|
|
7232
|
-
id: snapshotId,
|
|
7233
|
-
runId,
|
|
7234
|
-
queryId: unit.queryId,
|
|
7235
|
-
queryText: unit.queryText,
|
|
7236
|
-
provider: providerName,
|
|
7237
|
-
// `model` is what was REQUESTED and `served_model` is what
|
|
7238
|
-
// answered. The manifest froze the request, so it is the
|
|
7239
|
-
// authority here: an adapter that reports its own default rather
|
|
7240
|
-
// than what it was handed would otherwise overwrite the identity
|
|
7241
|
-
// the revision recorded, and nothing would show that it moved.
|
|
7242
|
-
model: unit.requestedModel ?? raw.model,
|
|
7243
|
-
servedModel: raw.servedModel ?? null,
|
|
7244
|
-
citationState,
|
|
7245
|
-
answerMentioned,
|
|
7246
|
-
answerText: normalized.answerText,
|
|
7247
|
-
citedDomains: normalized.citedDomains,
|
|
7248
|
-
citedUrls: citedUrlCapture.citedUrls,
|
|
7249
|
-
captureStatus: citedUrlCapture.captureStatus,
|
|
7250
|
-
sourceCount: citedUrlCapture.sourceCount,
|
|
7251
|
-
resolvedCount: citedUrlCapture.resolvedCount,
|
|
7252
|
-
captureVersion: citedUrlCapture.captureVersion,
|
|
7253
|
-
retrievalStatus: normalized.retrievalStatus,
|
|
7254
|
-
retrievalContract: raw.retrievalContract,
|
|
7255
|
-
competitorOverlap: overlap,
|
|
7256
|
-
recommendedCompetitors: extractedCompetitors,
|
|
7257
|
-
// Only claim the geography the provider actually honoured. A
|
|
7258
|
-
// requested-but-unsupported context stores `location: null` —
|
|
7259
|
-
// "no claim" — rather than the label we asked for, mirroring
|
|
7260
|
-
// `supportedContext` itself: this field is never non-null when
|
|
7261
|
-
// that one is null.
|
|
7262
|
-
location: supportedContext ? requestedContext?.label ?? null : null,
|
|
7263
|
-
measurementExecutionId: unit.executionId,
|
|
7264
|
-
requestedContext,
|
|
7265
|
-
supportedContext,
|
|
7266
|
-
screenshotPath: screenshotRelPath,
|
|
7267
|
-
rawResponse: JSON.stringify({
|
|
7268
|
-
model: raw.model,
|
|
7269
|
-
servedModel: raw.servedModel ?? null,
|
|
7270
|
-
groundingSources: normalized.groundingSources,
|
|
7271
|
-
searchQueries: normalized.searchQueries,
|
|
7272
|
-
apiResponse: raw.rawResponse
|
|
7273
|
-
}),
|
|
7274
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7275
|
-
}).run();
|
|
7276
|
-
totalSnapshotsInserted++;
|
|
7277
|
-
log2.info("query.citation", {
|
|
7278
|
-
runId,
|
|
7279
|
-
provider: providerName,
|
|
7280
|
-
query: unit.queryText,
|
|
7281
|
-
executionId: unit.executionId,
|
|
7282
|
-
location: requestedContext?.label ?? null,
|
|
7283
|
-
citationState,
|
|
7284
|
-
answerMentioned
|
|
7285
|
-
});
|
|
7286
|
-
});
|
|
7287
|
-
} catch (err) {
|
|
7288
|
-
if (err instanceof RunCancelledError) {
|
|
7289
|
-
throw err;
|
|
7290
|
-
}
|
|
7291
|
-
const msg = describeError(err);
|
|
7292
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
7293
|
-
log2.error("query.failed", { runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
|
|
7294
|
-
if (!providerErrors.has(providerName)) {
|
|
7295
|
-
providerErrors.set(providerName, msg);
|
|
7296
|
-
}
|
|
7170
|
+
const slotContext = {
|
|
7171
|
+
runId,
|
|
7172
|
+
allDomains,
|
|
7173
|
+
competitorDomains,
|
|
7174
|
+
competitorAliases: runCompetitors.aliases,
|
|
7175
|
+
allBrandNames,
|
|
7176
|
+
executionGates,
|
|
7177
|
+
providerDispatchCounts,
|
|
7178
|
+
providerErrors,
|
|
7179
|
+
onInserted: () => {
|
|
7180
|
+
totalSnapshotsInserted++;
|
|
7297
7181
|
}
|
|
7298
7182
|
};
|
|
7183
|
+
const processNodeForProvider = (registeredProvider, unit) => this.executePlanSlot(slotContext, registeredProvider, unit);
|
|
7299
7184
|
if (existingRun.kind === RunKinds["answer-visibility"] && existingRun.trigger !== RunTriggers.probe && existingRun.measurementPlanVersionId === null) {
|
|
7300
7185
|
const definition = buildSimpleMeasurementDefinition({
|
|
7301
7186
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7442,7 +7327,7 @@ var JobRunner = class {
|
|
|
7442
7327
|
status: "failed",
|
|
7443
7328
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7444
7329
|
error: errorMessage
|
|
7445
|
-
}).where(eq2(runs.id, runId)).run();
|
|
7330
|
+
}).where(and2(eq2(runs.id, runId), inArray(runs.status, ["queued", "running"]))).run();
|
|
7446
7331
|
this.flushProviderUsage(providerDispatchCounts, providerReservations);
|
|
7447
7332
|
const abortReason2 = classifyRunAbortReason(errorMessage);
|
|
7448
7333
|
const phases = buildPhases({ startTime, providerCallStart, providerCallEnd });
|
|
@@ -7483,6 +7368,366 @@ var JobRunner = class {
|
|
|
7483
7368
|
}
|
|
7484
7369
|
}
|
|
7485
7370
|
}
|
|
7371
|
+
/**
|
|
7372
|
+
* Complete a partial plan run in place: record only the expected slots it
|
|
7373
|
+
* never answered, under the same run id, then finalize the run once.
|
|
7374
|
+
*
|
|
7375
|
+
* The parent run is never written from an error path and only ever leaves
|
|
7376
|
+
* `partial` through the completion compare-and-set, so a failed or
|
|
7377
|
+
* interrupted fill leaves the run as the sweep left it, plus any answers the
|
|
7378
|
+
* fill did record. Its timestamps, manifest and identity are never touched:
|
|
7379
|
+
* it stays the same sweep, with the same place in every history.
|
|
7380
|
+
*/
|
|
7381
|
+
async executeRunFill(fillId) {
|
|
7382
|
+
const claim = this.db.update(runFills).set({ status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and2(eq2(runFills.id, fillId), eq2(runFills.status, "queued"))).run();
|
|
7383
|
+
if (claim.changes !== 1) {
|
|
7384
|
+
log2.warn("fill.not-claimed", { fillId });
|
|
7385
|
+
return;
|
|
7386
|
+
}
|
|
7387
|
+
try {
|
|
7388
|
+
await this.runClaimedFill(fillId);
|
|
7389
|
+
} catch (err) {
|
|
7390
|
+
const error = describeError(err);
|
|
7391
|
+
log2.error("fill.aborted", { fillId, error });
|
|
7392
|
+
this.db.update(runFills).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error }).where(and2(eq2(runFills.id, fillId), eq2(runFills.status, "running"))).run();
|
|
7393
|
+
}
|
|
7394
|
+
}
|
|
7395
|
+
async runClaimedFill(fillId) {
|
|
7396
|
+
const fill = this.db.select().from(runFills).where(eq2(runFills.id, fillId)).get();
|
|
7397
|
+
if (!fill) return;
|
|
7398
|
+
const { runId, projectId } = fill;
|
|
7399
|
+
const requested = new Set(parseJsonColumn(fill.providers, []).map((provider) => provider.trim().toLocaleLowerCase("en")));
|
|
7400
|
+
const providerDispatchCounts = /* @__PURE__ */ new Map();
|
|
7401
|
+
const providerReservations = /* @__PURE__ */ new Map();
|
|
7402
|
+
const providerErrors = /* @__PURE__ */ new Map();
|
|
7403
|
+
let filled = 0;
|
|
7404
|
+
let fatal = null;
|
|
7405
|
+
let superseded = false;
|
|
7406
|
+
try {
|
|
7407
|
+
const run = this.getRunState(runId);
|
|
7408
|
+
if (!run) throw new Error(`Run ${runId} not found`);
|
|
7409
|
+
if (run.status !== "partial") throw new Error(`Run ${runId} is ${run.status}; only a partial run can be filled`);
|
|
7410
|
+
const runCreatedAt = this.db.select({ createdAt: runs.createdAt }).from(runs).where(eq2(runs.id, runId)).get().createdAt;
|
|
7411
|
+
const project = this.db.select().from(projects).where(eq2(projects.id, projectId)).get();
|
|
7412
|
+
if (!project) throw new Error(`Project ${projectId} not found`);
|
|
7413
|
+
const projectQueries = this.db.select().from(queries).where(eq2(queries.projectId, projectId)).all();
|
|
7414
|
+
const plan = resolvePlanExecution(run, projectQueries);
|
|
7415
|
+
if (!plan) throw new Error(`Run ${runId} did not measure a published plan`);
|
|
7416
|
+
const recorded = new Set(
|
|
7417
|
+
this.db.select({ executionId: querySnapshots.measurementExecutionId, provider: querySnapshots.provider }).from(querySnapshots).where(eq2(querySnapshots.runId, runId)).all().flatMap((row) => row.executionId ? [measurementSlotKey(row.executionId, row.provider)] : [])
|
|
7418
|
+
);
|
|
7419
|
+
const unitsByProvider = /* @__PURE__ */ new Map();
|
|
7420
|
+
for (const [provider, units] of plan.unitsByProvider) {
|
|
7421
|
+
if (requested.size > 0 && !requested.has(provider)) continue;
|
|
7422
|
+
const missing = units.filter((unit) => !recorded.has(measurementSlotKey(unit.executionId, provider)));
|
|
7423
|
+
if (missing.length > 0) unitsByProvider.set(provider, missing);
|
|
7424
|
+
}
|
|
7425
|
+
const activeProviders = [];
|
|
7426
|
+
for (const provider of unitsByProvider.keys()) {
|
|
7427
|
+
const registered = this.registry.get(provider);
|
|
7428
|
+
if (registered) activeProviders.push(registered);
|
|
7429
|
+
else providerErrors.set(provider, `No ${provider} provider is configured on this instance, so its missing answers did not run.`);
|
|
7430
|
+
}
|
|
7431
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq2(competitors.projectId, projectId)).all();
|
|
7432
|
+
const runCompetitors = createRunCompetitorResolver(this.db, projectCompetitors.map((c) => c.domain))(run.measurementPlanVersionId);
|
|
7433
|
+
const competitorDomains = runCompetitors.domains;
|
|
7434
|
+
const allDomains = effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains });
|
|
7435
|
+
const allBrandNames = effectiveBrandNames({ displayName: project.displayName, aliases: project.aliases });
|
|
7436
|
+
const todayPeriod = getCurrentUsageDay();
|
|
7437
|
+
const dispatchable = [];
|
|
7438
|
+
for (const registered of activeProviders) {
|
|
7439
|
+
const name = registered.adapter.name;
|
|
7440
|
+
const count = unitsByProvider.get(name)?.length ?? 0;
|
|
7441
|
+
const scope = `${projectId}:${name}`;
|
|
7442
|
+
const limit = registered.config.quotaPolicy.maxRequestsPerDay;
|
|
7443
|
+
const quota = reserveDailyQueryQuota(this.db, { scope, period: todayPeriod, count, limit });
|
|
7444
|
+
if (!quota.reserved) {
|
|
7445
|
+
providerErrors.set(name, `Daily quota exceeded for ${name}: ${quota.used} queries used today, limit is ${limit}. This fill needs ${count} more.`);
|
|
7446
|
+
continue;
|
|
7447
|
+
}
|
|
7448
|
+
providerReservations.set(name, { scope, period: todayPeriod, reserved: count });
|
|
7449
|
+
dispatchable.push(registered);
|
|
7450
|
+
}
|
|
7451
|
+
const executionGates = /* @__PURE__ */ new Map();
|
|
7452
|
+
for (const registered of dispatchable) {
|
|
7453
|
+
executionGates.set(
|
|
7454
|
+
registered.adapter.name,
|
|
7455
|
+
getSharedProviderExecutionGate(
|
|
7456
|
+
registered.adapter.name,
|
|
7457
|
+
registered.config.quotaPolicy.maxConcurrency,
|
|
7458
|
+
registered.config.quotaPolicy.maxRequestsPerMinute
|
|
7459
|
+
)
|
|
7460
|
+
);
|
|
7461
|
+
}
|
|
7462
|
+
const consecutiveFailures = /* @__PURE__ */ new Map();
|
|
7463
|
+
const stopped = /* @__PURE__ */ new Set();
|
|
7464
|
+
const newerSweepExists = () => {
|
|
7465
|
+
if (!superseded && newerFullSweep(this.db, { id: runId, projectId, createdAt: runCreatedAt })) {
|
|
7466
|
+
superseded = true;
|
|
7467
|
+
log2.warn("fill.superseded", { fillId, runId });
|
|
7468
|
+
}
|
|
7469
|
+
return superseded;
|
|
7470
|
+
};
|
|
7471
|
+
let removed = false;
|
|
7472
|
+
const attemptRemoved = () => {
|
|
7473
|
+
if (!removed && this.db.select({ status: runFills.status }).from(runFills).where(eq2(runFills.id, fillId)).get()?.status !== "running") {
|
|
7474
|
+
removed = true;
|
|
7475
|
+
log2.warn("fill.attempt-removed", { fillId, runId });
|
|
7476
|
+
}
|
|
7477
|
+
return removed;
|
|
7478
|
+
};
|
|
7479
|
+
const slotRecorded = (provider, executionId) => this.db.select({ id: querySnapshots.id }).from(querySnapshots).where(and2(
|
|
7480
|
+
eq2(querySnapshots.runId, runId),
|
|
7481
|
+
eq2(querySnapshots.measurementExecutionId, executionId),
|
|
7482
|
+
eq2(querySnapshots.provider, provider)
|
|
7483
|
+
)).get() !== void 0;
|
|
7484
|
+
const ctx = {
|
|
7485
|
+
runId,
|
|
7486
|
+
allDomains,
|
|
7487
|
+
competitorDomains,
|
|
7488
|
+
competitorAliases: runCompetitors.aliases,
|
|
7489
|
+
allBrandNames,
|
|
7490
|
+
executionGates,
|
|
7491
|
+
providerDispatchCounts,
|
|
7492
|
+
providerErrors,
|
|
7493
|
+
onInserted: () => {
|
|
7494
|
+
filled++;
|
|
7495
|
+
},
|
|
7496
|
+
fill: {
|
|
7497
|
+
shouldSkip: (provider, executionId) => stopped.has(provider) || attemptRemoved() || newerSweepExists() || slotRecorded(provider, executionId),
|
|
7498
|
+
onOutcome: (provider, ok) => {
|
|
7499
|
+
if (ok) {
|
|
7500
|
+
consecutiveFailures.set(provider, 0);
|
|
7501
|
+
return;
|
|
7502
|
+
}
|
|
7503
|
+
const failures = (consecutiveFailures.get(provider) ?? 0) + 1;
|
|
7504
|
+
consecutiveFailures.set(provider, failures);
|
|
7505
|
+
if (failures >= RUN_FILL_PROVIDER_BREAKER && !stopped.has(provider)) {
|
|
7506
|
+
stopped.add(provider);
|
|
7507
|
+
log2.warn("fill.provider-stopped", { fillId, runId, provider, failures });
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7511
|
+
};
|
|
7512
|
+
log2.info("fill.dispatch", {
|
|
7513
|
+
fillId,
|
|
7514
|
+
runId,
|
|
7515
|
+
missing: Object.fromEntries([...unitsByProvider].map(([provider, units]) => [provider, units.length]))
|
|
7516
|
+
});
|
|
7517
|
+
const unitsFor = (registered) => unitsByProvider.get(registered.adapter.name) ?? [];
|
|
7518
|
+
const apiProviders = dispatchable.filter((p) => !isBrowserProvider(p.adapter.name));
|
|
7519
|
+
const browserProviders = dispatchable.filter((p) => isBrowserProvider(p.adapter.name));
|
|
7520
|
+
await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registered) => {
|
|
7521
|
+
await runWithConcurrency(
|
|
7522
|
+
unitsFor(registered),
|
|
7523
|
+
Math.max(1, registered.config.quotaPolicy.maxConcurrency),
|
|
7524
|
+
(unit) => this.executePlanSlot(ctx, registered, unit)
|
|
7525
|
+
);
|
|
7526
|
+
});
|
|
7527
|
+
for (const registered of browserProviders) {
|
|
7528
|
+
for (const unit of unitsFor(registered)) await this.executePlanSlot(ctx, registered, unit);
|
|
7529
|
+
}
|
|
7530
|
+
} catch (err) {
|
|
7531
|
+
fatal = describeError(err);
|
|
7532
|
+
log2.error("fill.failed", { fillId, runId, error: fatal });
|
|
7533
|
+
} finally {
|
|
7534
|
+
this.flushProviderUsage(providerDispatchCounts, providerReservations);
|
|
7535
|
+
}
|
|
7536
|
+
const outcome = this.finalizeRunFill({ fillId, runId, filled, fatal, providerErrors, superseded });
|
|
7537
|
+
log2.info("fill.finished", { fillId, runId, filled, ...outcome });
|
|
7538
|
+
if (outcome.justCompleted && !outcome.superseded && this.onRunCompleted) {
|
|
7539
|
+
this.onRunCompleted(runId, projectId, { origin: "fill" }).catch((err) => {
|
|
7540
|
+
log2.error("notification.callback-failed", { runId, error: describeError(err) });
|
|
7541
|
+
});
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
/** Record the fill's outcome and, if the run is now whole, complete it exactly once. */
|
|
7545
|
+
finalizeRunFill(input) {
|
|
7546
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7547
|
+
return this.db.transaction((tx) => {
|
|
7548
|
+
const txDb = tx;
|
|
7549
|
+
const parent = txDb.select({ projectId: runs.projectId, createdAt: runs.createdAt, error: runs.error }).from(runs).where(eq2(runs.id, input.runId)).get();
|
|
7550
|
+
const superseded = input.superseded || parent !== void 0 && newerFullSweep(txDb, { id: input.runId, projectId: parent.projectId, createdAt: parent.createdAt }) !== void 0;
|
|
7551
|
+
const state = measurementRunSlotState(txDb, input.runId);
|
|
7552
|
+
const complete3 = state.planned && state.readable && state.missing.length === 0 && !state.hasUnboundSnapshot;
|
|
7553
|
+
let justCompleted = false;
|
|
7554
|
+
if (complete3) {
|
|
7555
|
+
justCompleted = txDb.update(runs).set({ status: "completed", error: null }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run().changes === 1;
|
|
7556
|
+
} else if (state.readable && !input.fatal) {
|
|
7557
|
+
const previous = parseRunError(parent?.error)?.providers ?? {};
|
|
7558
|
+
const fresh = buildRunErrorFromMessages(input.providerErrors).providers ?? {};
|
|
7559
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
7560
|
+
for (const slot of state.missing) remaining.set(slot.provider, (remaining.get(slot.provider) ?? 0) + 1);
|
|
7561
|
+
const providers = {};
|
|
7562
|
+
for (const [provider, count] of remaining) {
|
|
7563
|
+
providers[provider] = fresh[provider] ?? previous[provider] ?? { message: `${count} expected measurement(s) have not run.` };
|
|
7564
|
+
}
|
|
7565
|
+
txDb.update(runs).set({ error: serializeRunError({ providers }) }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run();
|
|
7566
|
+
}
|
|
7567
|
+
const status = complete3 ? "completed" : input.filled > 0 ? "partial" : "failed";
|
|
7568
|
+
const reason = input.fatal ?? (superseded ? "Stopped because a newer sweep started; no answers were added behind it." : null) ?? (input.providerErrors.size > 0 ? formatRunErrorOneLine(buildRunErrorFromMessages(input.providerErrors)) : null);
|
|
7569
|
+
txDb.update(runFills).set({ status, filled: input.filled, error: complete3 ? null : reason, finishedAt }).where(eq2(runFills.id, input.fillId)).run();
|
|
7570
|
+
return { justCompleted, superseded };
|
|
7571
|
+
});
|
|
7572
|
+
}
|
|
7573
|
+
/**
|
|
7574
|
+
* The plan-aware unit of work: one execution node, one provider.
|
|
7575
|
+
*
|
|
7576
|
+
* Deliberately a separate worker from the legacy query-by-query path rather
|
|
7577
|
+
* than a generalization of it. The legacy path is the behaviour every
|
|
7578
|
+
* planless project already depends on; leaving its body alone is what
|
|
7579
|
+
* makes "planless is byte-identical" a fact rather than a hope.
|
|
7580
|
+
*
|
|
7581
|
+
* A method rather than a closure inside `executeRun` so a sweep and a fill
|
|
7582
|
+
* record a slot through the same code: a filled answer is indistinguishable
|
|
7583
|
+
* from one the sweep recorded itself, which is what lets it count.
|
|
7584
|
+
*/
|
|
7585
|
+
async executePlanSlot(ctx, registeredProvider, unit) {
|
|
7586
|
+
const { adapter, config: providerConfig } = registeredProvider;
|
|
7587
|
+
const providerName = adapter.name;
|
|
7588
|
+
const gate = ctx.executionGates.get(providerName);
|
|
7589
|
+
if (!gate) {
|
|
7590
|
+
throw new Error(`Missing execution gate for provider ${providerName}`);
|
|
7591
|
+
}
|
|
7592
|
+
const config = unit.requestedModel ? { ...providerConfig, model: unit.requestedModel } : providerConfig;
|
|
7593
|
+
const requestedContext = unit.context;
|
|
7594
|
+
const supportedContext = requestedContext && providerSupportsLocationContext(adapter) ? { status: "applied", resolved: requestedContext } : null;
|
|
7595
|
+
if (ctx.fill?.shouldSkip(providerName, unit.executionId)) return;
|
|
7596
|
+
try {
|
|
7597
|
+
await gate.run(async () => {
|
|
7598
|
+
this.throwIfRunCancelled(ctx.runId);
|
|
7599
|
+
if (ctx.fill?.shouldSkip(providerName, unit.executionId)) return;
|
|
7600
|
+
ctx.providerDispatchCounts.set(providerName, (ctx.providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
7601
|
+
const raw = await adapter.executeTrackedQuery(
|
|
7602
|
+
{
|
|
7603
|
+
query: unit.queryText,
|
|
7604
|
+
canonicalDomains: ctx.allDomains,
|
|
7605
|
+
competitorDomains: ctx.competitorDomains,
|
|
7606
|
+
location: requestedContext ?? void 0
|
|
7607
|
+
},
|
|
7608
|
+
config
|
|
7609
|
+
);
|
|
7610
|
+
this.throwIfRunCancelled(ctx.runId);
|
|
7611
|
+
const providerResult = adapter.normalizeResult(raw);
|
|
7612
|
+
const rawGroundingSources = providerResult.groundingSources;
|
|
7613
|
+
const normalized = {
|
|
7614
|
+
...providerResult,
|
|
7615
|
+
groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
|
|
7616
|
+
};
|
|
7617
|
+
let citedUrlCapture;
|
|
7618
|
+
try {
|
|
7619
|
+
citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
|
|
7620
|
+
} catch (err) {
|
|
7621
|
+
citedUrlCapture = {
|
|
7622
|
+
citedUrls: [],
|
|
7623
|
+
captureStatus: "failed",
|
|
7624
|
+
sourceCount: normalized.groundingSources.length,
|
|
7625
|
+
resolvedCount: 0,
|
|
7626
|
+
captureVersion: CITED_URL_CAPTURE_VERSION
|
|
7627
|
+
};
|
|
7628
|
+
log2.warn("query.cited-url-capture-failed", {
|
|
7629
|
+
runId: ctx.runId,
|
|
7630
|
+
provider: providerName,
|
|
7631
|
+
query: unit.queryText,
|
|
7632
|
+
error: describeError(err)
|
|
7633
|
+
});
|
|
7634
|
+
}
|
|
7635
|
+
this.throwIfRunCancelled(ctx.runId);
|
|
7636
|
+
const citationState = determineCitationState(normalized, ctx.allDomains);
|
|
7637
|
+
const answerMentioned = determineAnswerMentioned(
|
|
7638
|
+
normalized.answerText,
|
|
7639
|
+
ctx.allBrandNames,
|
|
7640
|
+
ctx.allDomains
|
|
7641
|
+
);
|
|
7642
|
+
const overlap = computeCompetitorOverlap(normalized, ctx.competitorDomains, ctx.competitorAliases);
|
|
7643
|
+
const extractedCompetitors = extractRecommendedCompetitors(
|
|
7644
|
+
normalized.answerText,
|
|
7645
|
+
ctx.allDomains,
|
|
7646
|
+
normalized.citedDomains,
|
|
7647
|
+
ctx.competitorDomains,
|
|
7648
|
+
ctx.allBrandNames,
|
|
7649
|
+
ctx.competitorAliases
|
|
7650
|
+
);
|
|
7651
|
+
const snapshotId = crypto6.randomUUID();
|
|
7652
|
+
let screenshotRelPath = null;
|
|
7653
|
+
if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
|
|
7654
|
+
const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", ctx.runId);
|
|
7655
|
+
if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
|
|
7656
|
+
const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
|
|
7657
|
+
fs3.renameSync(raw.screenshotPath, destPath);
|
|
7658
|
+
screenshotRelPath = `${ctx.runId}/${snapshotId}.png`;
|
|
7659
|
+
}
|
|
7660
|
+
const insert = this.db.insert(querySnapshots).values({
|
|
7661
|
+
id: snapshotId,
|
|
7662
|
+
runId: ctx.runId,
|
|
7663
|
+
queryId: unit.queryId,
|
|
7664
|
+
queryText: unit.queryText,
|
|
7665
|
+
provider: providerName,
|
|
7666
|
+
// `model` is what was REQUESTED and `served_model` is what
|
|
7667
|
+
// answered. The manifest froze the request, so it is the
|
|
7668
|
+
// authority here: an adapter that reports its own default rather
|
|
7669
|
+
// than what it was handed would otherwise overwrite the identity
|
|
7670
|
+
// the revision recorded, and nothing would show that it moved.
|
|
7671
|
+
model: unit.requestedModel ?? raw.model,
|
|
7672
|
+
servedModel: raw.servedModel ?? null,
|
|
7673
|
+
citationState,
|
|
7674
|
+
answerMentioned,
|
|
7675
|
+
answerText: normalized.answerText,
|
|
7676
|
+
citedDomains: normalized.citedDomains,
|
|
7677
|
+
citedUrls: citedUrlCapture.citedUrls,
|
|
7678
|
+
captureStatus: citedUrlCapture.captureStatus,
|
|
7679
|
+
sourceCount: citedUrlCapture.sourceCount,
|
|
7680
|
+
resolvedCount: citedUrlCapture.resolvedCount,
|
|
7681
|
+
captureVersion: citedUrlCapture.captureVersion,
|
|
7682
|
+
retrievalStatus: normalized.retrievalStatus,
|
|
7683
|
+
retrievalContract: raw.retrievalContract,
|
|
7684
|
+
competitorOverlap: overlap,
|
|
7685
|
+
recommendedCompetitors: extractedCompetitors,
|
|
7686
|
+
// Only claim the geography the provider actually honoured. A
|
|
7687
|
+
// requested-but-unsupported context stores `location: null` —
|
|
7688
|
+
// "no claim" — rather than the label we asked for, mirroring
|
|
7689
|
+
// `supportedContext` itself: this field is never non-null when
|
|
7690
|
+
// that one is null.
|
|
7691
|
+
location: supportedContext ? requestedContext?.label ?? null : null,
|
|
7692
|
+
measurementExecutionId: unit.executionId,
|
|
7693
|
+
requestedContext,
|
|
7694
|
+
supportedContext,
|
|
7695
|
+
screenshotPath: screenshotRelPath,
|
|
7696
|
+
rawResponse: JSON.stringify({
|
|
7697
|
+
model: raw.model,
|
|
7698
|
+
servedModel: raw.servedModel ?? null,
|
|
7699
|
+
groundingSources: normalized.groundingSources,
|
|
7700
|
+
searchQueries: normalized.searchQueries,
|
|
7701
|
+
apiResponse: raw.rawResponse
|
|
7702
|
+
}),
|
|
7703
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7704
|
+
});
|
|
7705
|
+
const written = ctx.fill ? insert.onConflictDoNothing().run() : insert.run();
|
|
7706
|
+
if (written.changes > 0) ctx.onInserted();
|
|
7707
|
+
ctx.fill?.onOutcome(providerName, true);
|
|
7708
|
+
log2.info("query.citation", {
|
|
7709
|
+
runId: ctx.runId,
|
|
7710
|
+
provider: providerName,
|
|
7711
|
+
query: unit.queryText,
|
|
7712
|
+
executionId: unit.executionId,
|
|
7713
|
+
location: requestedContext?.label ?? null,
|
|
7714
|
+
citationState,
|
|
7715
|
+
answerMentioned
|
|
7716
|
+
});
|
|
7717
|
+
});
|
|
7718
|
+
} catch (err) {
|
|
7719
|
+
if (err instanceof RunCancelledError) {
|
|
7720
|
+
throw err;
|
|
7721
|
+
}
|
|
7722
|
+
const msg = describeError(err);
|
|
7723
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
7724
|
+
log2.error("query.failed", { runId: ctx.runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
|
|
7725
|
+
if (!ctx.providerErrors.has(providerName)) {
|
|
7726
|
+
ctx.providerErrors.set(providerName, msg);
|
|
7727
|
+
}
|
|
7728
|
+
ctx.fill?.onOutcome(providerName, false);
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7486
7731
|
incrementUsage(scope, metric, count) {
|
|
7487
7732
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7488
7733
|
const period = now.slice(0, 10);
|
|
@@ -11329,10 +11574,11 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
11329
11574
|
let reparsed = 0;
|
|
11330
11575
|
let providerErrors = 0;
|
|
11331
11576
|
if (scopedProjects.length > 0) {
|
|
11332
|
-
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and13(
|
|
11577
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and13(
|
|
11333
11578
|
eq15(runs.kind, RunKinds["answer-visibility"]),
|
|
11334
11579
|
inArray6(runs.projectId, scopedProjects.map((project) => project.id))
|
|
11335
|
-
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq15(runs.kind, RunKinds["answer-visibility"])).all();
|
|
11580
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(eq15(runs.kind, RunKinds["answer-visibility"])).all();
|
|
11581
|
+
const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
|
|
11336
11582
|
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
11337
11583
|
for (const run of runRows) {
|
|
11338
11584
|
const existing = runIdsByProject.get(run.projectId);
|
|
@@ -11341,6 +11587,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
11341
11587
|
}
|
|
11342
11588
|
for (const project of scopedProjects) {
|
|
11343
11589
|
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq15(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
11590
|
+
const competitorsForRun = createRunCompetitorResolver(db, competitorDomains);
|
|
11344
11591
|
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
11345
11592
|
if (runIds.length === 0) continue;
|
|
11346
11593
|
const projectDomains = effectiveDomains({
|
|
@@ -11355,6 +11602,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
11355
11602
|
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
11356
11603
|
const snapshotRows = db.select({
|
|
11357
11604
|
id: querySnapshots.id,
|
|
11605
|
+
runId: querySnapshots.runId,
|
|
11358
11606
|
provider: querySnapshots.provider,
|
|
11359
11607
|
citationState: querySnapshots.citationState,
|
|
11360
11608
|
answerMentioned: querySnapshots.answerMentioned,
|
|
@@ -11395,13 +11643,15 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
11395
11643
|
};
|
|
11396
11644
|
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
11397
11645
|
const nextCitedDomains = reparsedResult.citedDomains;
|
|
11398
|
-
const
|
|
11646
|
+
const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId));
|
|
11647
|
+
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
|
|
11399
11648
|
const nextRecommendedCompetitors = extractRecommendedCompetitors(
|
|
11400
11649
|
normalized.answerText,
|
|
11401
11650
|
projectDomains,
|
|
11402
11651
|
normalized.citedDomains,
|
|
11403
|
-
|
|
11404
|
-
projectBrandNames
|
|
11652
|
+
runCompetitors.domains,
|
|
11653
|
+
projectBrandNames,
|
|
11654
|
+
runCompetitors.aliases
|
|
11405
11655
|
);
|
|
11406
11656
|
const nextRawResponse = stringifyStoredSnapshotEnvelope(
|
|
11407
11657
|
snapshot.rawResponse,
|
|
@@ -11628,8 +11878,10 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
11628
11878
|
const project = db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
11629
11879
|
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
11630
11880
|
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq15(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
11631
|
-
const runRows = db.select({ id: runs.id }).from(runs).where(and13(eq15(runs.kind, RunKinds["answer-visibility"]), eq15(runs.projectId, projectId))).all();
|
|
11881
|
+
const runRows = db.select({ id: runs.id, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and13(eq15(runs.kind, RunKinds["answer-visibility"]), eq15(runs.projectId, projectId))).all();
|
|
11632
11882
|
const runIds = runRows.map((r) => r.id);
|
|
11883
|
+
const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
|
|
11884
|
+
const competitorsForRun = createRunCompetitorResolver(db, competitorDomains);
|
|
11633
11885
|
let examined = 0;
|
|
11634
11886
|
let updated = 0;
|
|
11635
11887
|
let wouldUpdate = 0;
|
|
@@ -11649,6 +11901,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
11649
11901
|
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
11650
11902
|
const snapshotRows = db.select({
|
|
11651
11903
|
id: querySnapshots.id,
|
|
11904
|
+
runId: querySnapshots.runId,
|
|
11652
11905
|
provider: querySnapshots.provider,
|
|
11653
11906
|
answerMentioned: querySnapshots.answerMentioned,
|
|
11654
11907
|
answerText: querySnapshots.answerText,
|
|
@@ -11674,13 +11927,15 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
11674
11927
|
retrievalStatus: "unknown",
|
|
11675
11928
|
searchQueries: []
|
|
11676
11929
|
};
|
|
11677
|
-
const
|
|
11930
|
+
const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId));
|
|
11931
|
+
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
|
|
11678
11932
|
const nextRecommendedCompetitors = extractRecommendedCompetitors(
|
|
11679
11933
|
answerText,
|
|
11680
11934
|
projectDomains,
|
|
11681
11935
|
citedDomains,
|
|
11682
|
-
|
|
11683
|
-
projectBrandNames
|
|
11936
|
+
runCompetitors.domains,
|
|
11937
|
+
projectBrandNames,
|
|
11938
|
+
runCompetitors.aliases
|
|
11684
11939
|
);
|
|
11685
11940
|
const nextPatch = {};
|
|
11686
11941
|
if (snapshot.answerMentioned !== nextAnswerMentioned) {
|
|
@@ -11777,7 +12032,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
11777
12032
|
return result2;
|
|
11778
12033
|
}
|
|
11779
12034
|
async function backfillInsightsCommand(project, opts) {
|
|
11780
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
12035
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-CJKOXPOP.js");
|
|
11781
12036
|
const config = loadConfig();
|
|
11782
12037
|
const db = createClient(config.database);
|
|
11783
12038
|
migrate(db);
|
|
@@ -13024,8 +13279,8 @@ var Notifier = class {
|
|
|
13024
13279
|
this.serverUrl = serverUrl;
|
|
13025
13280
|
}
|
|
13026
13281
|
/** Called after a run completes (success, partial, or failed). */
|
|
13027
|
-
async onRunCompleted(runId, projectId) {
|
|
13028
|
-
log17.info("run.completed", { runId, projectId });
|
|
13282
|
+
async onRunCompleted(runId, projectId, opts) {
|
|
13283
|
+
log17.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
|
|
13029
13284
|
const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
13030
13285
|
if (notifs.length === 0) {
|
|
13031
13286
|
log17.info("notifications.none-enabled", { projectId });
|
|
@@ -13045,7 +13300,7 @@ var Notifier = class {
|
|
|
13045
13300
|
const transitions = this.computeTransitions(runId, projectId);
|
|
13046
13301
|
const events = [];
|
|
13047
13302
|
log17.info("run.status", { runId: run.id, status: run.status, projectId });
|
|
13048
|
-
if (run.status === "completed" || run.status === "partial") {
|
|
13303
|
+
if ((run.status === "completed" || run.status === "partial") && opts?.origin !== "fill") {
|
|
13049
13304
|
events.push("run.completed");
|
|
13050
13305
|
}
|
|
13051
13306
|
if (run.status === "failed") {
|
|
@@ -13496,7 +13751,7 @@ var RunCoordinator = class {
|
|
|
13496
13751
|
intelligenceService;
|
|
13497
13752
|
onInsightsGenerated;
|
|
13498
13753
|
onAeroEvent;
|
|
13499
|
-
async onRunCompleted(runId, projectId) {
|
|
13754
|
+
async onRunCompleted(runId, projectId, opts) {
|
|
13500
13755
|
const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
|
|
13501
13756
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
13502
13757
|
if (runRow?.trigger === RunTriggers.probe) {
|
|
@@ -13558,7 +13813,7 @@ var RunCoordinator = class {
|
|
|
13558
13813
|
}
|
|
13559
13814
|
}
|
|
13560
13815
|
try {
|
|
13561
|
-
await this.notifier.onRunCompleted(runId, projectId);
|
|
13816
|
+
await (opts ? this.notifier.onRunCompleted(runId, projectId, opts) : this.notifier.onRunCompleted(runId, projectId));
|
|
13562
13817
|
} catch (err) {
|
|
13563
13818
|
log18.error("notifier.failed", { runId, error: describeError(err) });
|
|
13564
13819
|
}
|
|
@@ -17990,7 +18245,7 @@ async function createServer(opts) {
|
|
|
17990
18245
|
void sessionRegistry.drainNow(project.name);
|
|
17991
18246
|
}
|
|
17992
18247
|
);
|
|
17993
|
-
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
18248
|
+
jobRunner.onRunCompleted = (runId, projectId, opts2) => opts2 ? runCoordinator.onRunCompleted(runId, projectId, opts2) : runCoordinator.onRunCompleted(runId, projectId);
|
|
17994
18249
|
const snapshotService = new SnapshotService(registry);
|
|
17995
18250
|
const liveProjectIds = new Set(
|
|
17996
18251
|
opts.db.select({ id: projects.id }).from(projects).all().map((project) => project.id)
|
|
@@ -19347,6 +19602,14 @@ async function createServer(opts) {
|
|
|
19347
19602
|
app.log.error({ runId, err }, "Job runner failed");
|
|
19348
19603
|
});
|
|
19349
19604
|
},
|
|
19605
|
+
getProviderDailyLimits: () => Object.fromEntries(
|
|
19606
|
+
registry.getAll().map((provider) => [provider.adapter.name, provider.config.quotaPolicy.maxRequestsPerDay])
|
|
19607
|
+
),
|
|
19608
|
+
onRunFillCreated: (fillId, runId) => {
|
|
19609
|
+
jobRunner.executeRunFill(fillId).catch((err) => {
|
|
19610
|
+
app.log.error({ fillId, runId, err: describeError(err) }, "Run fill failed");
|
|
19611
|
+
});
|
|
19612
|
+
},
|
|
19350
19613
|
onRunCancelled: (runId) => {
|
|
19351
19614
|
const controller = siteAuditAbortControllers.get(runId);
|
|
19352
19615
|
if (controller && !controller.signal.aborted) {
|