@canonry/canonry 5.11.0 → 5.13.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-Cq8CoZV5.js → AuditHistoryPanel-DWDOPf8c.js} +1 -1
- package/assets/assets/{BacklinksPage-B5_QeuXd.js → BacklinksPage-DGxq-iU6.js} +1 -1
- package/assets/assets/{HistoryPage-Foj5--rM.js → HistoryPage-DR91m-gT.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-2t8jVNFL.js → MeasurementPropertyPage-a8poltfM.js} +1 -1
- package/assets/assets/{ProjectPage-By0UYm8r.js → ProjectPage-CCCJE7rA.js} +1 -1
- package/assets/assets/{RunRow-CbTtzDK5.js → RunRow-Xe63DyPK.js} +1 -1
- package/assets/assets/{RunsPage-Cjv7YiXR.js → RunsPage-DK119ZZw.js} +1 -1
- package/assets/assets/{SettingsPage-DIQgJQ8Y.js → SettingsPage-G461foD-.js} +1 -1
- package/assets/assets/{SiteHealthSection-CUNhD0To.js → SiteHealthSection-CJnHkGM2.js} +3 -3
- package/assets/assets/{TrafficPage-abuZqCEU.js → TrafficPage-AsWgc7Ot.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-B-zVt0Sa.js → TrafficSourceDetailPage-D10yBQoD.js} +1 -1
- package/assets/assets/{extract-error-message-C5-X8IcG.js → extract-error-message-PP4HD_Dp.js} +1 -1
- package/assets/assets/{index-Dzg_siX8.js → index-CCIeAE4J.js} +27 -27
- package/assets/assets/{react-sigma_core.esm.min-Ybxr8Ybd.js → react-sigma_core.esm.min-CkyyYkKU.js} +1 -1
- package/assets/assets/{v2-overview-adapter-BMOnF8IE.js → v2-overview-adapter-DW-In1hp.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-NV3X6O5P.js → chunk-6YLNO2T5.js} +2 -2
- package/dist/{chunk-JNU6GLAV.js → chunk-IDZQ5H3J.js} +2 -2
- package/dist/{chunk-JW5DCORY.js → chunk-IPGBK7XR.js} +87 -1
- package/dist/{chunk-SMYBGUVV.js → chunk-Q35D2R2P.js} +2515 -2097
- package/dist/{chunk-UYSTNPTX.js → chunk-TDHRUXN3.js} +487 -172
- package/dist/{chunk-MQMJJZ6W.js → chunk-UWJAATGC.js} +3021 -2925
- package/dist/cli.js +133 -7
- package/dist/{demo-server-76IZBMJ3.js → demo-server-IZFTJI7E.js} +3 -3
- package/dist/index.d.ts +28 -1
- package/dist/index.js +5 -5
- package/dist/{intelligence-service-KF5WWPGR.js → intelligence-service-PM7MC5VC.js} +2 -2
- package/dist/mcp.js +3 -3
- package/package.json +8 -8
|
@@ -17,12 +17,12 @@ import {
|
|
|
17
17
|
resolveOperatorApiKeyIds,
|
|
18
18
|
runWithUsageTags,
|
|
19
19
|
saveConfigPatch
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-IPGBK7XR.js";
|
|
21
21
|
import {
|
|
22
22
|
deleteSiteCrawlGraphLayout,
|
|
23
23
|
persistSiteCrawlGraphLayout,
|
|
24
24
|
prepareSiteCrawlGraphLayout
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-IDZQ5H3J.js";
|
|
26
26
|
import {
|
|
27
27
|
PACKAGE_VERSION
|
|
28
28
|
} from "./chunk-AOJU5LKX.js";
|
|
@@ -134,7 +134,10 @@ import {
|
|
|
134
134
|
loadDuckdb,
|
|
135
135
|
markSessionFailed,
|
|
136
136
|
measurementRunCompleteness,
|
|
137
|
+
measurementRunSlotState,
|
|
138
|
+
measurementSlotKey,
|
|
137
139
|
migrate,
|
|
140
|
+
newerFullSweep,
|
|
138
141
|
nextRunFromCron,
|
|
139
142
|
nextRunFromSchedule,
|
|
140
143
|
notifications,
|
|
@@ -164,6 +167,7 @@ import {
|
|
|
164
167
|
resolveVercelSyncDeadlineMs,
|
|
165
168
|
resolveWebhookTarget,
|
|
166
169
|
runChecks,
|
|
170
|
+
runFills,
|
|
167
171
|
runs,
|
|
168
172
|
schedules,
|
|
169
173
|
serializeUserSessionCookie,
|
|
@@ -179,7 +183,7 @@ import {
|
|
|
179
183
|
siteLivenessState,
|
|
180
184
|
toAlertView,
|
|
181
185
|
usageCounters
|
|
182
|
-
} from "./chunk-
|
|
186
|
+
} from "./chunk-Q35D2R2P.js";
|
|
183
187
|
import {
|
|
184
188
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
185
189
|
AGENT_NONE,
|
|
@@ -211,6 +215,7 @@ import {
|
|
|
211
215
|
ONBOARDING_FLOW_VERSION,
|
|
212
216
|
ProviderNames,
|
|
213
217
|
ROUTE_CAPABLE_CITED_URL_PROVIDERS,
|
|
218
|
+
RUN_FILL_PROVIDER_BREAKER,
|
|
214
219
|
ResearchQueryStatuses,
|
|
215
220
|
ResearchRunStatuses,
|
|
216
221
|
RunKinds,
|
|
@@ -272,6 +277,7 @@ import {
|
|
|
272
277
|
filterCapturedCitedUrls,
|
|
273
278
|
forbidden,
|
|
274
279
|
formatIsoDateInTimeZone,
|
|
280
|
+
formatRunErrorOneLine,
|
|
275
281
|
frameAncestorsHeaderValue,
|
|
276
282
|
googleAdsAccessibleCustomerDtoSchema,
|
|
277
283
|
googleAdsAccessibleCustomersResponseSchema,
|
|
@@ -300,6 +306,7 @@ import {
|
|
|
300
306
|
isVertexGroundingRedirect,
|
|
301
307
|
mapWithConcurrency,
|
|
302
308
|
maskTelemetryAnonymousId,
|
|
309
|
+
missingDependency,
|
|
303
310
|
nextScheduleUpdatedAt,
|
|
304
311
|
normalizeAgentSlug,
|
|
305
312
|
normalizeIdTokens,
|
|
@@ -312,6 +319,7 @@ import {
|
|
|
312
319
|
observeTemplateLinkEdges,
|
|
313
320
|
parseMeasurementRunManifestV1,
|
|
314
321
|
parseOriginList,
|
|
322
|
+
parseRunError,
|
|
315
323
|
providerError,
|
|
316
324
|
providerSupportsLocationContext,
|
|
317
325
|
registrableDomain,
|
|
@@ -336,7 +344,7 @@ import {
|
|
|
336
344
|
visibilityReportRequestSchema,
|
|
337
345
|
winnabilityClassLabel,
|
|
338
346
|
withRetry
|
|
339
|
-
} from "./chunk-
|
|
347
|
+
} from "./chunk-UWJAATGC.js";
|
|
340
348
|
|
|
341
349
|
// src/runtime-context.ts
|
|
342
350
|
function cliRuntimeContext(env = process.env, stdio = process) {
|
|
@@ -6482,7 +6490,7 @@ function nonBlank(value) {
|
|
|
6482
6490
|
return trimmed ? trimmed : void 0;
|
|
6483
6491
|
}
|
|
6484
6492
|
function resolveBuildCommit(env = process.env) {
|
|
6485
|
-
const embedded = true ? "
|
|
6493
|
+
const embedded = true ? "894086f6a60be75c0ade892884adf57ef7a38291" : void 0;
|
|
6486
6494
|
return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
|
|
6487
6495
|
}
|
|
6488
6496
|
function resolveInstanceIdentity(env = process.env) {
|
|
@@ -6830,6 +6838,8 @@ var JobRunner = class {
|
|
|
6830
6838
|
this.registry = registry;
|
|
6831
6839
|
}
|
|
6832
6840
|
recoverStaleRuns() {
|
|
6841
|
+
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();
|
|
6842
|
+
if (staleFills.changes > 0) log2.warn("fill.recovered-stale", { count: staleFills.changes });
|
|
6833
6843
|
const stale = this.db.select({
|
|
6834
6844
|
id: runs.id,
|
|
6835
6845
|
projectId: runs.projectId,
|
|
@@ -7155,146 +7165,19 @@ var JobRunner = class {
|
|
|
7155
7165
|
}
|
|
7156
7166
|
}
|
|
7157
7167
|
};
|
|
7158
|
-
const
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
try {
|
|
7169
|
-
await gate.run(async () => {
|
|
7170
|
-
this.throwIfRunCancelled(runId);
|
|
7171
|
-
providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
7172
|
-
const raw = await adapter.executeTrackedQuery(
|
|
7173
|
-
{
|
|
7174
|
-
query: unit.queryText,
|
|
7175
|
-
canonicalDomains: allDomains,
|
|
7176
|
-
competitorDomains,
|
|
7177
|
-
location: requestedContext ?? void 0
|
|
7178
|
-
},
|
|
7179
|
-
config
|
|
7180
|
-
);
|
|
7181
|
-
this.throwIfRunCancelled(runId);
|
|
7182
|
-
const providerResult = adapter.normalizeResult(raw);
|
|
7183
|
-
const rawGroundingSources = providerResult.groundingSources;
|
|
7184
|
-
const normalized = {
|
|
7185
|
-
...providerResult,
|
|
7186
|
-
groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
|
|
7187
|
-
};
|
|
7188
|
-
let citedUrlCapture;
|
|
7189
|
-
try {
|
|
7190
|
-
citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
|
|
7191
|
-
} catch (err) {
|
|
7192
|
-
citedUrlCapture = {
|
|
7193
|
-
citedUrls: [],
|
|
7194
|
-
captureStatus: "failed",
|
|
7195
|
-
sourceCount: normalized.groundingSources.length,
|
|
7196
|
-
resolvedCount: 0,
|
|
7197
|
-
captureVersion: CITED_URL_CAPTURE_VERSION
|
|
7198
|
-
};
|
|
7199
|
-
log2.warn("query.cited-url-capture-failed", {
|
|
7200
|
-
runId,
|
|
7201
|
-
provider: providerName,
|
|
7202
|
-
query: unit.queryText,
|
|
7203
|
-
error: describeError(err)
|
|
7204
|
-
});
|
|
7205
|
-
}
|
|
7206
|
-
this.throwIfRunCancelled(runId);
|
|
7207
|
-
const citationState = determineCitationState(normalized, allDomains);
|
|
7208
|
-
const answerMentioned = determineAnswerMentioned(
|
|
7209
|
-
normalized.answerText,
|
|
7210
|
-
allBrandNames,
|
|
7211
|
-
allDomains
|
|
7212
|
-
);
|
|
7213
|
-
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
7214
|
-
const extractedCompetitors = extractRecommendedCompetitors(
|
|
7215
|
-
normalized.answerText,
|
|
7216
|
-
allDomains,
|
|
7217
|
-
normalized.citedDomains,
|
|
7218
|
-
competitorDomains,
|
|
7219
|
-
allBrandNames
|
|
7220
|
-
);
|
|
7221
|
-
const snapshotId = crypto6.randomUUID();
|
|
7222
|
-
let screenshotRelPath = null;
|
|
7223
|
-
if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
|
|
7224
|
-
const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
|
|
7225
|
-
if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
|
|
7226
|
-
const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
|
|
7227
|
-
fs3.renameSync(raw.screenshotPath, destPath);
|
|
7228
|
-
screenshotRelPath = `${runId}/${snapshotId}.png`;
|
|
7229
|
-
}
|
|
7230
|
-
this.db.insert(querySnapshots).values({
|
|
7231
|
-
id: snapshotId,
|
|
7232
|
-
runId,
|
|
7233
|
-
queryId: unit.queryId,
|
|
7234
|
-
queryText: unit.queryText,
|
|
7235
|
-
provider: providerName,
|
|
7236
|
-
// `model` is what was REQUESTED and `served_model` is what
|
|
7237
|
-
// answered. The manifest froze the request, so it is the
|
|
7238
|
-
// authority here: an adapter that reports its own default rather
|
|
7239
|
-
// than what it was handed would otherwise overwrite the identity
|
|
7240
|
-
// the revision recorded, and nothing would show that it moved.
|
|
7241
|
-
model: unit.requestedModel ?? raw.model,
|
|
7242
|
-
servedModel: raw.servedModel ?? null,
|
|
7243
|
-
citationState,
|
|
7244
|
-
answerMentioned,
|
|
7245
|
-
answerText: normalized.answerText,
|
|
7246
|
-
citedDomains: normalized.citedDomains,
|
|
7247
|
-
citedUrls: citedUrlCapture.citedUrls,
|
|
7248
|
-
captureStatus: citedUrlCapture.captureStatus,
|
|
7249
|
-
sourceCount: citedUrlCapture.sourceCount,
|
|
7250
|
-
resolvedCount: citedUrlCapture.resolvedCount,
|
|
7251
|
-
captureVersion: citedUrlCapture.captureVersion,
|
|
7252
|
-
retrievalStatus: normalized.retrievalStatus,
|
|
7253
|
-
retrievalContract: raw.retrievalContract,
|
|
7254
|
-
competitorOverlap: overlap,
|
|
7255
|
-
recommendedCompetitors: extractedCompetitors,
|
|
7256
|
-
// Only claim the geography the provider actually honoured. A
|
|
7257
|
-
// requested-but-unsupported context stores `location: null` —
|
|
7258
|
-
// "no claim" — rather than the label we asked for, mirroring
|
|
7259
|
-
// `supportedContext` itself: this field is never non-null when
|
|
7260
|
-
// that one is null.
|
|
7261
|
-
location: supportedContext ? requestedContext?.label ?? null : null,
|
|
7262
|
-
measurementExecutionId: unit.executionId,
|
|
7263
|
-
requestedContext,
|
|
7264
|
-
supportedContext,
|
|
7265
|
-
screenshotPath: screenshotRelPath,
|
|
7266
|
-
rawResponse: JSON.stringify({
|
|
7267
|
-
model: raw.model,
|
|
7268
|
-
servedModel: raw.servedModel ?? null,
|
|
7269
|
-
groundingSources: normalized.groundingSources,
|
|
7270
|
-
searchQueries: normalized.searchQueries,
|
|
7271
|
-
apiResponse: raw.rawResponse
|
|
7272
|
-
}),
|
|
7273
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7274
|
-
}).run();
|
|
7275
|
-
totalSnapshotsInserted++;
|
|
7276
|
-
log2.info("query.citation", {
|
|
7277
|
-
runId,
|
|
7278
|
-
provider: providerName,
|
|
7279
|
-
query: unit.queryText,
|
|
7280
|
-
executionId: unit.executionId,
|
|
7281
|
-
location: requestedContext?.label ?? null,
|
|
7282
|
-
citationState,
|
|
7283
|
-
answerMentioned
|
|
7284
|
-
});
|
|
7285
|
-
});
|
|
7286
|
-
} catch (err) {
|
|
7287
|
-
if (err instanceof RunCancelledError) {
|
|
7288
|
-
throw err;
|
|
7289
|
-
}
|
|
7290
|
-
const msg = describeError(err);
|
|
7291
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
7292
|
-
log2.error("query.failed", { runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
|
|
7293
|
-
if (!providerErrors.has(providerName)) {
|
|
7294
|
-
providerErrors.set(providerName, msg);
|
|
7295
|
-
}
|
|
7168
|
+
const slotContext = {
|
|
7169
|
+
runId,
|
|
7170
|
+
allDomains,
|
|
7171
|
+
competitorDomains,
|
|
7172
|
+
allBrandNames,
|
|
7173
|
+
executionGates,
|
|
7174
|
+
providerDispatchCounts,
|
|
7175
|
+
providerErrors,
|
|
7176
|
+
onInserted: () => {
|
|
7177
|
+
totalSnapshotsInserted++;
|
|
7296
7178
|
}
|
|
7297
7179
|
};
|
|
7180
|
+
const processNodeForProvider = (registeredProvider, unit) => this.executePlanSlot(slotContext, registeredProvider, unit);
|
|
7298
7181
|
if (existingRun.kind === RunKinds["answer-visibility"] && existingRun.trigger !== RunTriggers.probe && existingRun.measurementPlanVersionId === null) {
|
|
7299
7182
|
const definition = buildSimpleMeasurementDefinition({
|
|
7300
7183
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7441,7 +7324,7 @@ var JobRunner = class {
|
|
|
7441
7324
|
status: "failed",
|
|
7442
7325
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7443
7326
|
error: errorMessage
|
|
7444
|
-
}).where(eq2(runs.id, runId)).run();
|
|
7327
|
+
}).where(and2(eq2(runs.id, runId), inArray(runs.status, ["queued", "running"]))).run();
|
|
7445
7328
|
this.flushProviderUsage(providerDispatchCounts, providerReservations);
|
|
7446
7329
|
const abortReason2 = classifyRunAbortReason(errorMessage);
|
|
7447
7330
|
const phases = buildPhases({ startTime, providerCallStart, providerCallEnd });
|
|
@@ -7482,6 +7365,363 @@ var JobRunner = class {
|
|
|
7482
7365
|
}
|
|
7483
7366
|
}
|
|
7484
7367
|
}
|
|
7368
|
+
/**
|
|
7369
|
+
* Complete a partial plan run in place: record only the expected slots it
|
|
7370
|
+
* never answered, under the same run id, then finalize the run once.
|
|
7371
|
+
*
|
|
7372
|
+
* The parent run is never written from an error path and only ever leaves
|
|
7373
|
+
* `partial` through the completion compare-and-set, so a failed or
|
|
7374
|
+
* interrupted fill leaves the run as the sweep left it, plus any answers the
|
|
7375
|
+
* fill did record. Its timestamps, manifest and identity are never touched:
|
|
7376
|
+
* it stays the same sweep, with the same place in every history.
|
|
7377
|
+
*/
|
|
7378
|
+
async executeRunFill(fillId) {
|
|
7379
|
+
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();
|
|
7380
|
+
if (claim.changes !== 1) {
|
|
7381
|
+
log2.warn("fill.not-claimed", { fillId });
|
|
7382
|
+
return;
|
|
7383
|
+
}
|
|
7384
|
+
try {
|
|
7385
|
+
await this.runClaimedFill(fillId);
|
|
7386
|
+
} catch (err) {
|
|
7387
|
+
const error = describeError(err);
|
|
7388
|
+
log2.error("fill.aborted", { fillId, error });
|
|
7389
|
+
this.db.update(runFills).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error }).where(and2(eq2(runFills.id, fillId), eq2(runFills.status, "running"))).run();
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
async runClaimedFill(fillId) {
|
|
7393
|
+
const fill = this.db.select().from(runFills).where(eq2(runFills.id, fillId)).get();
|
|
7394
|
+
if (!fill) return;
|
|
7395
|
+
const { runId, projectId } = fill;
|
|
7396
|
+
const requested = new Set(parseJsonColumn(fill.providers, []).map((provider) => provider.trim().toLocaleLowerCase("en")));
|
|
7397
|
+
const providerDispatchCounts = /* @__PURE__ */ new Map();
|
|
7398
|
+
const providerReservations = /* @__PURE__ */ new Map();
|
|
7399
|
+
const providerErrors = /* @__PURE__ */ new Map();
|
|
7400
|
+
let filled = 0;
|
|
7401
|
+
let fatal = null;
|
|
7402
|
+
let superseded = false;
|
|
7403
|
+
try {
|
|
7404
|
+
const run = this.getRunState(runId);
|
|
7405
|
+
if (!run) throw new Error(`Run ${runId} not found`);
|
|
7406
|
+
if (run.status !== "partial") throw new Error(`Run ${runId} is ${run.status}; only a partial run can be filled`);
|
|
7407
|
+
const runCreatedAt = this.db.select({ createdAt: runs.createdAt }).from(runs).where(eq2(runs.id, runId)).get().createdAt;
|
|
7408
|
+
const project = this.db.select().from(projects).where(eq2(projects.id, projectId)).get();
|
|
7409
|
+
if (!project) throw new Error(`Project ${projectId} not found`);
|
|
7410
|
+
const projectQueries = this.db.select().from(queries).where(eq2(queries.projectId, projectId)).all();
|
|
7411
|
+
const plan = resolvePlanExecution(run, projectQueries);
|
|
7412
|
+
if (!plan) throw new Error(`Run ${runId} did not measure a published plan`);
|
|
7413
|
+
const recorded = new Set(
|
|
7414
|
+
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)] : [])
|
|
7415
|
+
);
|
|
7416
|
+
const unitsByProvider = /* @__PURE__ */ new Map();
|
|
7417
|
+
for (const [provider, units] of plan.unitsByProvider) {
|
|
7418
|
+
if (requested.size > 0 && !requested.has(provider)) continue;
|
|
7419
|
+
const missing = units.filter((unit) => !recorded.has(measurementSlotKey(unit.executionId, provider)));
|
|
7420
|
+
if (missing.length > 0) unitsByProvider.set(provider, missing);
|
|
7421
|
+
}
|
|
7422
|
+
const activeProviders = [];
|
|
7423
|
+
for (const provider of unitsByProvider.keys()) {
|
|
7424
|
+
const registered = this.registry.get(provider);
|
|
7425
|
+
if (registered) activeProviders.push(registered);
|
|
7426
|
+
else providerErrors.set(provider, `No ${provider} provider is configured on this instance, so its missing answers did not run.`);
|
|
7427
|
+
}
|
|
7428
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq2(competitors.projectId, projectId)).all();
|
|
7429
|
+
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
7430
|
+
const allDomains = effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains });
|
|
7431
|
+
const allBrandNames = effectiveBrandNames({ displayName: project.displayName, aliases: project.aliases });
|
|
7432
|
+
const todayPeriod = getCurrentUsageDay();
|
|
7433
|
+
const dispatchable = [];
|
|
7434
|
+
for (const registered of activeProviders) {
|
|
7435
|
+
const name = registered.adapter.name;
|
|
7436
|
+
const count = unitsByProvider.get(name)?.length ?? 0;
|
|
7437
|
+
const scope = `${projectId}:${name}`;
|
|
7438
|
+
const limit = registered.config.quotaPolicy.maxRequestsPerDay;
|
|
7439
|
+
const quota = reserveDailyQueryQuota(this.db, { scope, period: todayPeriod, count, limit });
|
|
7440
|
+
if (!quota.reserved) {
|
|
7441
|
+
providerErrors.set(name, `Daily quota exceeded for ${name}: ${quota.used} queries used today, limit is ${limit}. This fill needs ${count} more.`);
|
|
7442
|
+
continue;
|
|
7443
|
+
}
|
|
7444
|
+
providerReservations.set(name, { scope, period: todayPeriod, reserved: count });
|
|
7445
|
+
dispatchable.push(registered);
|
|
7446
|
+
}
|
|
7447
|
+
const executionGates = /* @__PURE__ */ new Map();
|
|
7448
|
+
for (const registered of dispatchable) {
|
|
7449
|
+
executionGates.set(
|
|
7450
|
+
registered.adapter.name,
|
|
7451
|
+
getSharedProviderExecutionGate(
|
|
7452
|
+
registered.adapter.name,
|
|
7453
|
+
registered.config.quotaPolicy.maxConcurrency,
|
|
7454
|
+
registered.config.quotaPolicy.maxRequestsPerMinute
|
|
7455
|
+
)
|
|
7456
|
+
);
|
|
7457
|
+
}
|
|
7458
|
+
const consecutiveFailures = /* @__PURE__ */ new Map();
|
|
7459
|
+
const stopped = /* @__PURE__ */ new Set();
|
|
7460
|
+
const newerSweepExists = () => {
|
|
7461
|
+
if (!superseded && newerFullSweep(this.db, { id: runId, projectId, createdAt: runCreatedAt })) {
|
|
7462
|
+
superseded = true;
|
|
7463
|
+
log2.warn("fill.superseded", { fillId, runId });
|
|
7464
|
+
}
|
|
7465
|
+
return superseded;
|
|
7466
|
+
};
|
|
7467
|
+
let removed = false;
|
|
7468
|
+
const attemptRemoved = () => {
|
|
7469
|
+
if (!removed && this.db.select({ status: runFills.status }).from(runFills).where(eq2(runFills.id, fillId)).get()?.status !== "running") {
|
|
7470
|
+
removed = true;
|
|
7471
|
+
log2.warn("fill.attempt-removed", { fillId, runId });
|
|
7472
|
+
}
|
|
7473
|
+
return removed;
|
|
7474
|
+
};
|
|
7475
|
+
const slotRecorded = (provider, executionId) => this.db.select({ id: querySnapshots.id }).from(querySnapshots).where(and2(
|
|
7476
|
+
eq2(querySnapshots.runId, runId),
|
|
7477
|
+
eq2(querySnapshots.measurementExecutionId, executionId),
|
|
7478
|
+
eq2(querySnapshots.provider, provider)
|
|
7479
|
+
)).get() !== void 0;
|
|
7480
|
+
const ctx = {
|
|
7481
|
+
runId,
|
|
7482
|
+
allDomains,
|
|
7483
|
+
competitorDomains,
|
|
7484
|
+
allBrandNames,
|
|
7485
|
+
executionGates,
|
|
7486
|
+
providerDispatchCounts,
|
|
7487
|
+
providerErrors,
|
|
7488
|
+
onInserted: () => {
|
|
7489
|
+
filled++;
|
|
7490
|
+
},
|
|
7491
|
+
fill: {
|
|
7492
|
+
shouldSkip: (provider, executionId) => stopped.has(provider) || attemptRemoved() || newerSweepExists() || slotRecorded(provider, executionId),
|
|
7493
|
+
onOutcome: (provider, ok) => {
|
|
7494
|
+
if (ok) {
|
|
7495
|
+
consecutiveFailures.set(provider, 0);
|
|
7496
|
+
return;
|
|
7497
|
+
}
|
|
7498
|
+
const failures = (consecutiveFailures.get(provider) ?? 0) + 1;
|
|
7499
|
+
consecutiveFailures.set(provider, failures);
|
|
7500
|
+
if (failures >= RUN_FILL_PROVIDER_BREAKER && !stopped.has(provider)) {
|
|
7501
|
+
stopped.add(provider);
|
|
7502
|
+
log2.warn("fill.provider-stopped", { fillId, runId, provider, failures });
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
};
|
|
7507
|
+
log2.info("fill.dispatch", {
|
|
7508
|
+
fillId,
|
|
7509
|
+
runId,
|
|
7510
|
+
missing: Object.fromEntries([...unitsByProvider].map(([provider, units]) => [provider, units.length]))
|
|
7511
|
+
});
|
|
7512
|
+
const unitsFor = (registered) => unitsByProvider.get(registered.adapter.name) ?? [];
|
|
7513
|
+
const apiProviders = dispatchable.filter((p) => !isBrowserProvider(p.adapter.name));
|
|
7514
|
+
const browserProviders = dispatchable.filter((p) => isBrowserProvider(p.adapter.name));
|
|
7515
|
+
await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registered) => {
|
|
7516
|
+
await runWithConcurrency(
|
|
7517
|
+
unitsFor(registered),
|
|
7518
|
+
Math.max(1, registered.config.quotaPolicy.maxConcurrency),
|
|
7519
|
+
(unit) => this.executePlanSlot(ctx, registered, unit)
|
|
7520
|
+
);
|
|
7521
|
+
});
|
|
7522
|
+
for (const registered of browserProviders) {
|
|
7523
|
+
for (const unit of unitsFor(registered)) await this.executePlanSlot(ctx, registered, unit);
|
|
7524
|
+
}
|
|
7525
|
+
} catch (err) {
|
|
7526
|
+
fatal = describeError(err);
|
|
7527
|
+
log2.error("fill.failed", { fillId, runId, error: fatal });
|
|
7528
|
+
} finally {
|
|
7529
|
+
this.flushProviderUsage(providerDispatchCounts, providerReservations);
|
|
7530
|
+
}
|
|
7531
|
+
const outcome = this.finalizeRunFill({ fillId, runId, filled, fatal, providerErrors, superseded });
|
|
7532
|
+
log2.info("fill.finished", { fillId, runId, filled, ...outcome });
|
|
7533
|
+
if (outcome.justCompleted && !outcome.superseded && this.onRunCompleted) {
|
|
7534
|
+
this.onRunCompleted(runId, projectId, { origin: "fill" }).catch((err) => {
|
|
7535
|
+
log2.error("notification.callback-failed", { runId, error: describeError(err) });
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
/** Record the fill's outcome and, if the run is now whole, complete it exactly once. */
|
|
7540
|
+
finalizeRunFill(input) {
|
|
7541
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7542
|
+
return this.db.transaction((tx) => {
|
|
7543
|
+
const txDb = tx;
|
|
7544
|
+
const parent = txDb.select({ projectId: runs.projectId, createdAt: runs.createdAt, error: runs.error }).from(runs).where(eq2(runs.id, input.runId)).get();
|
|
7545
|
+
const superseded = input.superseded || parent !== void 0 && newerFullSweep(txDb, { id: input.runId, projectId: parent.projectId, createdAt: parent.createdAt }) !== void 0;
|
|
7546
|
+
const state = measurementRunSlotState(txDb, input.runId);
|
|
7547
|
+
const complete3 = state.planned && state.readable && state.missing.length === 0 && !state.hasUnboundSnapshot;
|
|
7548
|
+
let justCompleted = false;
|
|
7549
|
+
if (complete3) {
|
|
7550
|
+
justCompleted = txDb.update(runs).set({ status: "completed", error: null }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run().changes === 1;
|
|
7551
|
+
} else if (state.readable && !input.fatal) {
|
|
7552
|
+
const previous = parseRunError(parent?.error)?.providers ?? {};
|
|
7553
|
+
const fresh = buildRunErrorFromMessages(input.providerErrors).providers ?? {};
|
|
7554
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
7555
|
+
for (const slot of state.missing) remaining.set(slot.provider, (remaining.get(slot.provider) ?? 0) + 1);
|
|
7556
|
+
const providers = {};
|
|
7557
|
+
for (const [provider, count] of remaining) {
|
|
7558
|
+
providers[provider] = fresh[provider] ?? previous[provider] ?? { message: `${count} expected measurement(s) have not run.` };
|
|
7559
|
+
}
|
|
7560
|
+
txDb.update(runs).set({ error: serializeRunError({ providers }) }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run();
|
|
7561
|
+
}
|
|
7562
|
+
const status = complete3 ? "completed" : input.filled > 0 ? "partial" : "failed";
|
|
7563
|
+
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);
|
|
7564
|
+
txDb.update(runFills).set({ status, filled: input.filled, error: complete3 ? null : reason, finishedAt }).where(eq2(runFills.id, input.fillId)).run();
|
|
7565
|
+
return { justCompleted, superseded };
|
|
7566
|
+
});
|
|
7567
|
+
}
|
|
7568
|
+
/**
|
|
7569
|
+
* The plan-aware unit of work: one execution node, one provider.
|
|
7570
|
+
*
|
|
7571
|
+
* Deliberately a separate worker from the legacy query-by-query path rather
|
|
7572
|
+
* than a generalization of it. The legacy path is the behaviour every
|
|
7573
|
+
* planless project already depends on; leaving its body alone is what
|
|
7574
|
+
* makes "planless is byte-identical" a fact rather than a hope.
|
|
7575
|
+
*
|
|
7576
|
+
* A method rather than a closure inside `executeRun` so a sweep and a fill
|
|
7577
|
+
* record a slot through the same code: a filled answer is indistinguishable
|
|
7578
|
+
* from one the sweep recorded itself, which is what lets it count.
|
|
7579
|
+
*/
|
|
7580
|
+
async executePlanSlot(ctx, registeredProvider, unit) {
|
|
7581
|
+
const { adapter, config: providerConfig } = registeredProvider;
|
|
7582
|
+
const providerName = adapter.name;
|
|
7583
|
+
const gate = ctx.executionGates.get(providerName);
|
|
7584
|
+
if (!gate) {
|
|
7585
|
+
throw new Error(`Missing execution gate for provider ${providerName}`);
|
|
7586
|
+
}
|
|
7587
|
+
const config = unit.requestedModel ? { ...providerConfig, model: unit.requestedModel } : providerConfig;
|
|
7588
|
+
const requestedContext = unit.context;
|
|
7589
|
+
const supportedContext = requestedContext && providerSupportsLocationContext(adapter) ? { status: "applied", resolved: requestedContext } : null;
|
|
7590
|
+
if (ctx.fill?.shouldSkip(providerName, unit.executionId)) return;
|
|
7591
|
+
try {
|
|
7592
|
+
await gate.run(async () => {
|
|
7593
|
+
this.throwIfRunCancelled(ctx.runId);
|
|
7594
|
+
if (ctx.fill?.shouldSkip(providerName, unit.executionId)) return;
|
|
7595
|
+
ctx.providerDispatchCounts.set(providerName, (ctx.providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
7596
|
+
const raw = await adapter.executeTrackedQuery(
|
|
7597
|
+
{
|
|
7598
|
+
query: unit.queryText,
|
|
7599
|
+
canonicalDomains: ctx.allDomains,
|
|
7600
|
+
competitorDomains: ctx.competitorDomains,
|
|
7601
|
+
location: requestedContext ?? void 0
|
|
7602
|
+
},
|
|
7603
|
+
config
|
|
7604
|
+
);
|
|
7605
|
+
this.throwIfRunCancelled(ctx.runId);
|
|
7606
|
+
const providerResult = adapter.normalizeResult(raw);
|
|
7607
|
+
const rawGroundingSources = providerResult.groundingSources;
|
|
7608
|
+
const normalized = {
|
|
7609
|
+
...providerResult,
|
|
7610
|
+
groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
|
|
7611
|
+
};
|
|
7612
|
+
let citedUrlCapture;
|
|
7613
|
+
try {
|
|
7614
|
+
citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
|
|
7615
|
+
} catch (err) {
|
|
7616
|
+
citedUrlCapture = {
|
|
7617
|
+
citedUrls: [],
|
|
7618
|
+
captureStatus: "failed",
|
|
7619
|
+
sourceCount: normalized.groundingSources.length,
|
|
7620
|
+
resolvedCount: 0,
|
|
7621
|
+
captureVersion: CITED_URL_CAPTURE_VERSION
|
|
7622
|
+
};
|
|
7623
|
+
log2.warn("query.cited-url-capture-failed", {
|
|
7624
|
+
runId: ctx.runId,
|
|
7625
|
+
provider: providerName,
|
|
7626
|
+
query: unit.queryText,
|
|
7627
|
+
error: describeError(err)
|
|
7628
|
+
});
|
|
7629
|
+
}
|
|
7630
|
+
this.throwIfRunCancelled(ctx.runId);
|
|
7631
|
+
const citationState = determineCitationState(normalized, ctx.allDomains);
|
|
7632
|
+
const answerMentioned = determineAnswerMentioned(
|
|
7633
|
+
normalized.answerText,
|
|
7634
|
+
ctx.allBrandNames,
|
|
7635
|
+
ctx.allDomains
|
|
7636
|
+
);
|
|
7637
|
+
const overlap = computeCompetitorOverlap(normalized, ctx.competitorDomains);
|
|
7638
|
+
const extractedCompetitors = extractRecommendedCompetitors(
|
|
7639
|
+
normalized.answerText,
|
|
7640
|
+
ctx.allDomains,
|
|
7641
|
+
normalized.citedDomains,
|
|
7642
|
+
ctx.competitorDomains,
|
|
7643
|
+
ctx.allBrandNames
|
|
7644
|
+
);
|
|
7645
|
+
const snapshotId = crypto6.randomUUID();
|
|
7646
|
+
let screenshotRelPath = null;
|
|
7647
|
+
if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
|
|
7648
|
+
const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", ctx.runId);
|
|
7649
|
+
if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
|
|
7650
|
+
const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
|
|
7651
|
+
fs3.renameSync(raw.screenshotPath, destPath);
|
|
7652
|
+
screenshotRelPath = `${ctx.runId}/${snapshotId}.png`;
|
|
7653
|
+
}
|
|
7654
|
+
const insert = this.db.insert(querySnapshots).values({
|
|
7655
|
+
id: snapshotId,
|
|
7656
|
+
runId: ctx.runId,
|
|
7657
|
+
queryId: unit.queryId,
|
|
7658
|
+
queryText: unit.queryText,
|
|
7659
|
+
provider: providerName,
|
|
7660
|
+
// `model` is what was REQUESTED and `served_model` is what
|
|
7661
|
+
// answered. The manifest froze the request, so it is the
|
|
7662
|
+
// authority here: an adapter that reports its own default rather
|
|
7663
|
+
// than what it was handed would otherwise overwrite the identity
|
|
7664
|
+
// the revision recorded, and nothing would show that it moved.
|
|
7665
|
+
model: unit.requestedModel ?? raw.model,
|
|
7666
|
+
servedModel: raw.servedModel ?? null,
|
|
7667
|
+
citationState,
|
|
7668
|
+
answerMentioned,
|
|
7669
|
+
answerText: normalized.answerText,
|
|
7670
|
+
citedDomains: normalized.citedDomains,
|
|
7671
|
+
citedUrls: citedUrlCapture.citedUrls,
|
|
7672
|
+
captureStatus: citedUrlCapture.captureStatus,
|
|
7673
|
+
sourceCount: citedUrlCapture.sourceCount,
|
|
7674
|
+
resolvedCount: citedUrlCapture.resolvedCount,
|
|
7675
|
+
captureVersion: citedUrlCapture.captureVersion,
|
|
7676
|
+
retrievalStatus: normalized.retrievalStatus,
|
|
7677
|
+
retrievalContract: raw.retrievalContract,
|
|
7678
|
+
competitorOverlap: overlap,
|
|
7679
|
+
recommendedCompetitors: extractedCompetitors,
|
|
7680
|
+
// Only claim the geography the provider actually honoured. A
|
|
7681
|
+
// requested-but-unsupported context stores `location: null` —
|
|
7682
|
+
// "no claim" — rather than the label we asked for, mirroring
|
|
7683
|
+
// `supportedContext` itself: this field is never non-null when
|
|
7684
|
+
// that one is null.
|
|
7685
|
+
location: supportedContext ? requestedContext?.label ?? null : null,
|
|
7686
|
+
measurementExecutionId: unit.executionId,
|
|
7687
|
+
requestedContext,
|
|
7688
|
+
supportedContext,
|
|
7689
|
+
screenshotPath: screenshotRelPath,
|
|
7690
|
+
rawResponse: JSON.stringify({
|
|
7691
|
+
model: raw.model,
|
|
7692
|
+
servedModel: raw.servedModel ?? null,
|
|
7693
|
+
groundingSources: normalized.groundingSources,
|
|
7694
|
+
searchQueries: normalized.searchQueries,
|
|
7695
|
+
apiResponse: raw.rawResponse
|
|
7696
|
+
}),
|
|
7697
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7698
|
+
});
|
|
7699
|
+
const written = ctx.fill ? insert.onConflictDoNothing().run() : insert.run();
|
|
7700
|
+
if (written.changes > 0) ctx.onInserted();
|
|
7701
|
+
ctx.fill?.onOutcome(providerName, true);
|
|
7702
|
+
log2.info("query.citation", {
|
|
7703
|
+
runId: ctx.runId,
|
|
7704
|
+
provider: providerName,
|
|
7705
|
+
query: unit.queryText,
|
|
7706
|
+
executionId: unit.executionId,
|
|
7707
|
+
location: requestedContext?.label ?? null,
|
|
7708
|
+
citationState,
|
|
7709
|
+
answerMentioned
|
|
7710
|
+
});
|
|
7711
|
+
});
|
|
7712
|
+
} catch (err) {
|
|
7713
|
+
if (err instanceof RunCancelledError) {
|
|
7714
|
+
throw err;
|
|
7715
|
+
}
|
|
7716
|
+
const msg = describeError(err);
|
|
7717
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
7718
|
+
log2.error("query.failed", { runId: ctx.runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
|
|
7719
|
+
if (!ctx.providerErrors.has(providerName)) {
|
|
7720
|
+
ctx.providerErrors.set(providerName, msg);
|
|
7721
|
+
}
|
|
7722
|
+
ctx.fill?.onOutcome(providerName, false);
|
|
7723
|
+
}
|
|
7724
|
+
}
|
|
7485
7725
|
incrementUsage(scope, metric, count) {
|
|
7486
7726
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7487
7727
|
const period = now.slice(0, 10);
|
|
@@ -11776,7 +12016,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
11776
12016
|
return result2;
|
|
11777
12017
|
}
|
|
11778
12018
|
async function backfillInsightsCommand(project, opts) {
|
|
11779
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
12019
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-PM7MC5VC.js");
|
|
11780
12020
|
const config = loadConfig();
|
|
11781
12021
|
const db = createClient(config.database);
|
|
11782
12022
|
migrate(db);
|
|
@@ -13023,8 +13263,8 @@ var Notifier = class {
|
|
|
13023
13263
|
this.serverUrl = serverUrl;
|
|
13024
13264
|
}
|
|
13025
13265
|
/** Called after a run completes (success, partial, or failed). */
|
|
13026
|
-
async onRunCompleted(runId, projectId) {
|
|
13027
|
-
log17.info("run.completed", { runId, projectId });
|
|
13266
|
+
async onRunCompleted(runId, projectId, opts) {
|
|
13267
|
+
log17.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
|
|
13028
13268
|
const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
13029
13269
|
if (notifs.length === 0) {
|
|
13030
13270
|
log17.info("notifications.none-enabled", { projectId });
|
|
@@ -13044,7 +13284,7 @@ var Notifier = class {
|
|
|
13044
13284
|
const transitions = this.computeTransitions(runId, projectId);
|
|
13045
13285
|
const events = [];
|
|
13046
13286
|
log17.info("run.status", { runId: run.id, status: run.status, projectId });
|
|
13047
|
-
if (run.status === "completed" || run.status === "partial") {
|
|
13287
|
+
if ((run.status === "completed" || run.status === "partial") && opts?.origin !== "fill") {
|
|
13048
13288
|
events.push("run.completed");
|
|
13049
13289
|
}
|
|
13050
13290
|
if (run.status === "failed") {
|
|
@@ -13495,7 +13735,7 @@ var RunCoordinator = class {
|
|
|
13495
13735
|
intelligenceService;
|
|
13496
13736
|
onInsightsGenerated;
|
|
13497
13737
|
onAeroEvent;
|
|
13498
|
-
async onRunCompleted(runId, projectId) {
|
|
13738
|
+
async onRunCompleted(runId, projectId, opts) {
|
|
13499
13739
|
const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
|
|
13500
13740
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
13501
13741
|
if (runRow?.trigger === RunTriggers.probe) {
|
|
@@ -13557,7 +13797,7 @@ var RunCoordinator = class {
|
|
|
13557
13797
|
}
|
|
13558
13798
|
}
|
|
13559
13799
|
try {
|
|
13560
|
-
await this.notifier.onRunCompleted(runId, projectId);
|
|
13800
|
+
await (opts ? this.notifier.onRunCompleted(runId, projectId, opts) : this.notifier.onRunCompleted(runId, projectId));
|
|
13561
13801
|
} catch (err) {
|
|
13562
13802
|
log18.error("notifier.failed", { runId, error: describeError(err) });
|
|
13563
13803
|
}
|
|
@@ -13865,13 +14105,15 @@ function resolveAgentId(providerOrPiAi) {
|
|
|
13865
14105
|
return void 0;
|
|
13866
14106
|
}
|
|
13867
14107
|
function buildAgentProvidersResponse(config) {
|
|
14108
|
+
const pinned = coerceAgentProvider(config.agent?.provider ?? void 0);
|
|
14109
|
+
const pinnedModel = pinned ? config.agent?.model ?? void 0 : void 0;
|
|
13868
14110
|
const providers = listAgentProviders().map((id) => {
|
|
13869
14111
|
const entry = AGENT_PROVIDERS[id];
|
|
13870
14112
|
const source = resolveApiKeySource(id, config);
|
|
13871
14113
|
return {
|
|
13872
14114
|
id,
|
|
13873
14115
|
label: entry.label,
|
|
13874
|
-
defaultModel: entry.defaultModel,
|
|
14116
|
+
defaultModel: id === pinned && pinnedModel ? pinnedModel : entry.defaultModel,
|
|
13875
14117
|
configured: source !== void 0,
|
|
13876
14118
|
keySource: source?.source ?? null
|
|
13877
14119
|
};
|
|
@@ -13879,7 +14121,7 @@ function buildAgentProvidersResponse(config) {
|
|
|
13879
14121
|
const firstConfigured = agentProvidersByPriority().find((p) => resolveApiKeySource(p, config));
|
|
13880
14122
|
return {
|
|
13881
14123
|
providers,
|
|
13882
|
-
defaultProvider: firstConfigured ?? null
|
|
14124
|
+
defaultProvider: pinned ?? firstConfigured ?? null
|
|
13883
14125
|
};
|
|
13884
14126
|
}
|
|
13885
14127
|
|
|
@@ -14845,6 +15087,27 @@ function missingProviderMessage() {
|
|
|
14845
15087
|
const envHints = agentProvidersByPriority().map(agentProviderApiKeyEnvVar).join(" / ");
|
|
14846
15088
|
return `No agent LLM provider configured. Add an API key for one of: ${configHints} in ~/.canonry/config.yaml, or export ${envHints}.`;
|
|
14847
15089
|
}
|
|
15090
|
+
function resolveConfiguredAgentProvider(config) {
|
|
15091
|
+
return coerceAgentProvider(config.agent?.provider);
|
|
15092
|
+
}
|
|
15093
|
+
function describeAgentPin(config) {
|
|
15094
|
+
const provider = resolveConfiguredAgentProvider(config);
|
|
15095
|
+
if (!provider) return null;
|
|
15096
|
+
const model = config.agent?.model ?? getAgentProvider(provider).defaultModel;
|
|
15097
|
+
let modelError = null;
|
|
15098
|
+
try {
|
|
15099
|
+
resolveAeroModel(provider, model);
|
|
15100
|
+
} catch (err) {
|
|
15101
|
+
modelError = err instanceof Error ? err.message : String(err);
|
|
15102
|
+
}
|
|
15103
|
+
return {
|
|
15104
|
+
provider,
|
|
15105
|
+
model,
|
|
15106
|
+
configured: Boolean(resolveApiKeyFor(provider, config)),
|
|
15107
|
+
envVar: agentProviderApiKeyEnvVar(provider),
|
|
15108
|
+
modelError
|
|
15109
|
+
};
|
|
15110
|
+
}
|
|
14848
15111
|
function detectAgentProvider(config) {
|
|
14849
15112
|
for (const provider of agentProvidersByPriority()) {
|
|
14850
15113
|
if (resolveApiKeyFor(provider, config)) return provider;
|
|
@@ -14915,9 +15178,11 @@ function createAeroSession(opts) {
|
|
|
14915
15178
|
return agent;
|
|
14916
15179
|
}
|
|
14917
15180
|
function resolveSessionProviderAndModel(config, opts) {
|
|
14918
|
-
const
|
|
15181
|
+
const pinned = resolveConfiguredAgentProvider(config);
|
|
15182
|
+
const provider = opts?.provider ?? pinned ?? detectAgentProvider(config);
|
|
14919
15183
|
if (!provider) throw new Error(missingProviderMessage());
|
|
14920
|
-
const
|
|
15184
|
+
const pinnedModelId = provider === pinned ? config.agent?.model : void 0;
|
|
15185
|
+
const modelId = opts?.modelId ?? pinnedModelId ?? getAgentProvider(provider).defaultModel;
|
|
14921
15186
|
return { provider, modelId };
|
|
14922
15187
|
}
|
|
14923
15188
|
|
|
@@ -15346,9 +15611,8 @@ var SessionRegistry = class {
|
|
|
15346
15611
|
if (row.systemPrompt !== systemPrompt) {
|
|
15347
15612
|
this.persistPromptSnapshot(projectId, systemPrompt);
|
|
15348
15613
|
}
|
|
15349
|
-
const effectiveProvider = preferences
|
|
15350
|
-
|
|
15351
|
-
if (preferences?.provider || preferences?.modelId) {
|
|
15614
|
+
const { provider: effectiveProvider, modelId: effectiveModelId } = this.resolveTurnModel(preferences, { provider: row.modelProvider, modelId: row.modelId });
|
|
15615
|
+
if (effectiveProvider !== row.modelProvider || effectiveModelId !== row.modelId) {
|
|
15352
15616
|
this.opts.db.update(agentSessions).set({
|
|
15353
15617
|
modelProvider: effectiveProvider,
|
|
15354
15618
|
modelId: effectiveModelId,
|
|
@@ -15481,17 +15745,19 @@ ${lines.join("\n")}
|
|
|
15481
15745
|
* - align `state.tools` to the requested scope/profile (CLI full vs
|
|
15482
15746
|
* dashboard read-only share the same cached Agent; each request
|
|
15483
15747
|
* re-scopes it).
|
|
15484
|
-
* - align `state.model`
|
|
15485
|
-
*
|
|
15486
|
-
*
|
|
15748
|
+
* - align `state.model` on every turn (see `resolveTurnModel`): an
|
|
15749
|
+
* explicit `provider` / `modelId` wins for this turn, then the
|
|
15750
|
+
* `agent.provider` pin, then the session's stored model.
|
|
15751
|
+
* - refuse, naming `agent.provider`, a pinned provider that has no key.
|
|
15487
15752
|
*
|
|
15488
|
-
* Persists the
|
|
15489
|
-
*
|
|
15753
|
+
* Persists the resolved model to the DB row as provenance. Without a pin an
|
|
15754
|
+
* override therefore sticks, as it always has; with a pin, the next turn
|
|
15755
|
+
* that names no provider returns to it.
|
|
15490
15756
|
*/
|
|
15491
15757
|
async acquireForTurn(projectName, preferences) {
|
|
15492
15758
|
preferences?.signal?.throwIfAborted();
|
|
15493
15759
|
if (this.acquisitions.has(projectName)) throw agentBusy(projectName);
|
|
15494
|
-
const agent = this.getOrCreate(projectName);
|
|
15760
|
+
const agent = this.getOrCreate(projectName, preferences);
|
|
15495
15761
|
if (agent.state.isStreaming) {
|
|
15496
15762
|
throw agentBusy(projectName);
|
|
15497
15763
|
}
|
|
@@ -15508,9 +15774,8 @@ ${lines.join("\n")}
|
|
|
15508
15774
|
scope: preferences?.toolScope ?? AeroToolScopes.all,
|
|
15509
15775
|
profile: preferences?.toolProfile ?? AeroToolProfiles.default
|
|
15510
15776
|
});
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
}
|
|
15777
|
+
this.alignModel(projectName, agent, preferences ?? {});
|
|
15778
|
+
this.assertTurnProviderUsable(projectId);
|
|
15514
15779
|
await this.mergeExternalTools(agent);
|
|
15515
15780
|
const view = { client: this.opts.client, projectName, basePath: this.opts.config.basePath, context: preferences?.context };
|
|
15516
15781
|
const evidence = preferences?.context ? await readAeroViewEvidence(view) : void 0;
|
|
@@ -15589,15 +15854,56 @@ ${lines.join("\n")}
|
|
|
15589
15854
|
this.scopes.set(projectName, want.scope);
|
|
15590
15855
|
this.profiles.set(projectName, want.profile);
|
|
15591
15856
|
}
|
|
15857
|
+
/**
|
|
15858
|
+
* Throw a named, actionable error when the `agent.provider` pin chose this
|
|
15859
|
+
* turn's provider and that provider has no key. Auto-detection only ever
|
|
15860
|
+
* picks a keyed provider, so the pin is the one path that can land a turn on
|
|
15861
|
+
* a keyless one without anything saying so. An explicitly requested provider
|
|
15862
|
+
* keeps its existing behaviour: the caller named it, and the provider's own
|
|
15863
|
+
* error reports the missing key when the prompt is sent.
|
|
15864
|
+
*/
|
|
15865
|
+
assertTurnProviderUsable(projectId) {
|
|
15866
|
+
const provider = coerceAgentProvider(this.loadRow(projectId)?.modelProvider ?? void 0);
|
|
15867
|
+
if (!provider || provider !== resolveConfiguredAgentProvider(this.opts.config)) return;
|
|
15868
|
+
if (resolveApiKeyFor(provider, this.opts.config)) return;
|
|
15869
|
+
throw missingDependency(
|
|
15870
|
+
`Aero is pinned to ${provider} by agent.provider, but no key is configured for it. Set providers.${provider}.apiKey in config.yaml or export ${agentProviderApiKeyEnvVar(provider)}, or change agent.provider.`,
|
|
15871
|
+
{ provider, pinned: true }
|
|
15872
|
+
);
|
|
15873
|
+
}
|
|
15874
|
+
/**
|
|
15875
|
+
* Resolve the provider/model a turn should use: an explicit request outranks
|
|
15876
|
+
* the `agent.provider` pin, which outranks whatever the row last recorded.
|
|
15877
|
+
*
|
|
15878
|
+
* The row is provenance (what answered), not policy (what answers next), so
|
|
15879
|
+
* a configured pin reasserts itself every turn instead of being frozen at
|
|
15880
|
+
* whatever the first session happened to pick -- and survives the
|
|
15881
|
+
* conversation-delete path, which drops the row entirely. With a pin, an
|
|
15882
|
+
* explicit request therefore lasts one turn.
|
|
15883
|
+
*
|
|
15884
|
+
* The model is a function of the request and the config, never of which
|
|
15885
|
+
* provider ran last: naming the pinned provider gets `agent.model` whether or
|
|
15886
|
+
* not the session just came back from another one. The row's model is used
|
|
15887
|
+
* only when nothing is pinned and nothing was requested, which keeps an
|
|
15888
|
+
* unpinned install's override sticky as it always was. A model id is never
|
|
15889
|
+
* forwarded across providers: any other case takes the provider's default.
|
|
15890
|
+
*/
|
|
15891
|
+
resolveTurnModel(preferences, current) {
|
|
15892
|
+
const pinned = resolveConfiguredAgentProvider(this.opts.config);
|
|
15893
|
+
const provider = preferences?.provider ?? pinned ?? current.provider ?? AgentProviderIds.claude;
|
|
15894
|
+
const carried = !preferences?.provider && !pinned && provider === current.provider ? current.modelId ?? void 0 : void 0;
|
|
15895
|
+
const pinnedModelId = provider === pinned ? this.opts.config.agent?.model ?? void 0 : void 0;
|
|
15896
|
+
const modelId = preferences?.modelId ?? pinnedModelId ?? carried ?? getAgentProvider(provider).defaultModel;
|
|
15897
|
+
return { provider, modelId };
|
|
15898
|
+
}
|
|
15592
15899
|
alignModel(projectName, agent, preferences) {
|
|
15593
15900
|
const projectId = this.tryResolveProjectId(projectName);
|
|
15594
15901
|
if (!projectId) return;
|
|
15595
15902
|
const row = this.loadRow(projectId);
|
|
15596
|
-
|
|
15597
|
-
const
|
|
15598
|
-
const
|
|
15599
|
-
const nextModelId = preferences
|
|
15600
|
-
if (!nextModelId) return;
|
|
15903
|
+
if (!row) return;
|
|
15904
|
+
const currentProvider = row.modelProvider;
|
|
15905
|
+
const currentModelId = row.modelId;
|
|
15906
|
+
const { provider: nextProvider, modelId: nextModelId } = this.resolveTurnModel(preferences, { provider: row.modelProvider, modelId: row.modelId });
|
|
15601
15907
|
if (nextProvider === currentProvider && nextModelId === currentModelId) return;
|
|
15602
15908
|
agent.state.model = resolveAeroModel(nextProvider, nextModelId);
|
|
15603
15909
|
this.opts.db.update(agentSessions).set({
|
|
@@ -17923,7 +18229,7 @@ async function createServer(opts) {
|
|
|
17923
18229
|
void sessionRegistry.drainNow(project.name);
|
|
17924
18230
|
}
|
|
17925
18231
|
);
|
|
17926
|
-
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
18232
|
+
jobRunner.onRunCompleted = (runId, projectId, opts2) => opts2 ? runCoordinator.onRunCompleted(runId, projectId, opts2) : runCoordinator.onRunCompleted(runId, projectId);
|
|
17927
18233
|
const snapshotService = new SnapshotService(registry);
|
|
17928
18234
|
const liveProjectIds = new Set(
|
|
17929
18235
|
opts.db.select({ id: projects.id }).from(projects).all().map((project) => project.id)
|
|
@@ -18981,6 +19287,7 @@ async function createServer(opts) {
|
|
|
18981
19287
|
// Resolved fresh each call so a key added at runtime (settings API) shows
|
|
18982
19288
|
// up immediately in the `config.agent-providers` doctor check.
|
|
18983
19289
|
getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
|
|
19290
|
+
getAgentPin: () => describeAgentPin(opts.config),
|
|
18984
19291
|
getGoogleMarketingDoctorInput: (ctx) => {
|
|
18985
19292
|
if (!ctx.project) return null;
|
|
18986
19293
|
const googleAds = opts.db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
|
|
@@ -19279,6 +19586,14 @@ async function createServer(opts) {
|
|
|
19279
19586
|
app.log.error({ runId, err }, "Job runner failed");
|
|
19280
19587
|
});
|
|
19281
19588
|
},
|
|
19589
|
+
getProviderDailyLimits: () => Object.fromEntries(
|
|
19590
|
+
registry.getAll().map((provider) => [provider.adapter.name, provider.config.quotaPolicy.maxRequestsPerDay])
|
|
19591
|
+
),
|
|
19592
|
+
onRunFillCreated: (fillId, runId) => {
|
|
19593
|
+
jobRunner.executeRunFill(fillId).catch((err) => {
|
|
19594
|
+
app.log.error({ fillId, runId, err: describeError(err) }, "Run fill failed");
|
|
19595
|
+
});
|
|
19596
|
+
},
|
|
19282
19597
|
onRunCancelled: (runId) => {
|
|
19283
19598
|
const controller = siteAuditAbortControllers.get(runId);
|
|
19284
19599
|
if (controller && !controller.signal.aborted) {
|