@ainyc/canonry 4.25.0 → 4.26.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/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-C4scWriC.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-X1r0qycv.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-rPok6yk8.css">
17
17
  </head>
18
18
  <body>
@@ -18,7 +18,7 @@ import {
18
18
  trafficConnectCloudRunRequestSchema,
19
19
  trafficConnectWordpressRequestSchema,
20
20
  trafficEventKindSchema
21
- } from "./chunk-CRQMGNPH.js";
21
+ } from "./chunk-HVW665A4.js";
22
22
 
23
23
  // src/config.ts
24
24
  import fs from "fs";
@@ -1954,7 +1954,7 @@ var canonryMcpTools = [
1954
1954
  defineTool({
1955
1955
  name: "canonry_run_trigger",
1956
1956
  title: "Trigger run",
1957
- description: "Trigger an answer-visibility run for a Canonry project.",
1957
+ description: "Trigger an answer-visibility run for a Canonry project. Pass request.queries[] to scope the sweep to a subset of the project's tracked queries; omit for a full sweep.",
1958
1958
  access: "write",
1959
1959
  tier: "core",
1960
1960
  inputSchema: runTriggerInputSchema,
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-A7HQ6X43.js";
8
+ } from "./chunk-2FAEQ56I.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -68,7 +68,7 @@ import {
68
68
  schedules,
69
69
  trafficSources,
70
70
  usageCounters
71
- } from "./chunk-IS65IYNZ.js";
71
+ } from "./chunk-PN24DAGC.js";
72
72
  import {
73
73
  AGENT_MEMORY_VALUE_MAX_BYTES,
74
74
  AGENT_PROVIDER_IDS,
@@ -169,7 +169,7 @@ import {
169
169
  visibilityStateFromAnswerMentioned,
170
170
  windowCutoff,
171
171
  wordpressEnvSchema
172
- } from "./chunk-CRQMGNPH.js";
172
+ } from "./chunk-HVW665A4.js";
173
173
 
174
174
  // src/telemetry.ts
175
175
  import crypto from "crypto";
@@ -1155,6 +1155,7 @@ function queueRunIfProjectIdle(db, params) {
1155
1155
  status: "queued",
1156
1156
  trigger,
1157
1157
  location: params.location ?? null,
1158
+ queries: params.queries ?? null,
1158
1159
  createdAt
1159
1160
  }).run();
1160
1161
  return { conflict: false, runId };
@@ -1185,6 +1186,20 @@ async function runRoutes(app, opts) {
1185
1186
  rawProviders.splice(0, rawProviders.length, ...normalized);
1186
1187
  }
1187
1188
  const providers = rawProviders?.length ? rawProviders : void 0;
1189
+ let scopedQueries = null;
1190
+ if (body.queries?.length) {
1191
+ const trackedRows = app.db.select({ query: queries.query }).from(queries).where(eq7(queries.projectId, project.id)).all();
1192
+ const tracked = new Set(trackedRows.map((r) => r.query));
1193
+ const missing = body.queries.filter((q) => !tracked.has(q));
1194
+ if (missing.length) {
1195
+ throw validationError(`Queries not tracked on project "${project.name}": ${missing.join(", ")}`, {
1196
+ missing,
1197
+ tracked: [...tracked]
1198
+ });
1199
+ }
1200
+ scopedQueries = body.queries;
1201
+ }
1202
+ const queriesColumn = scopedQueries ? JSON.stringify(scopedQueries) : null;
1188
1203
  let resolvedLocation;
1189
1204
  const projectLocations = parseJsonColumn(project.locations, []);
1190
1205
  if (body.noLocation) {
@@ -1217,6 +1232,7 @@ async function runRoutes(app, opts) {
1217
1232
  status: "queued",
1218
1233
  trigger,
1219
1234
  location: loc.label,
1235
+ queries: queriesColumn,
1220
1236
  createdAt: now
1221
1237
  }).run();
1222
1238
  newRuns.push({ runId: runId2, loc });
@@ -1244,7 +1260,8 @@ async function runRoutes(app, opts) {
1244
1260
  kind,
1245
1261
  projectId: project.id,
1246
1262
  trigger,
1247
- location: locationLabel
1263
+ location: locationLabel,
1264
+ queries: queriesColumn
1248
1265
  });
1249
1266
  if (queueResult.conflict) throw runInProgress(project.name);
1250
1267
  const runId = queueResult.runId;
@@ -1390,6 +1407,7 @@ function formatRun(row) {
1390
1407
  status: row.status,
1391
1408
  trigger: row.trigger,
1392
1409
  location: row.location,
1410
+ queries: parseJsonColumn(row.queries, null),
1393
1411
  startedAt: row.startedAt,
1394
1412
  finishedAt: row.finishedAt,
1395
1413
  error: parseRunError(row.error),
@@ -8003,6 +8021,7 @@ var routeCatalog = [
8003
8021
  kind: stringSchema,
8004
8022
  trigger: stringSchema,
8005
8023
  providers: stringArraySchema,
8024
+ queries: stringArraySchema,
8006
8025
  location: stringSchema,
8007
8026
  allLocations: booleanSchema,
8008
8027
  noLocation: booleanSchema
@@ -10171,8 +10190,8 @@ var routeCatalog = [
10171
10190
  {
10172
10191
  method: "post",
10173
10192
  path: "/api/v1/projects/{name}/traffic/sources/{id}/backfill",
10174
- summary: "Reclassify historical Cloud Run logs for a traffic source",
10175
- description: 'Async one-shot backfill: pulls the last `days` of request logs (clamped server-side to the upstream retention ceiling \u2014 30d for Cloud Logging `_Default`), classifies them with the current rules, and replaces the hourly rollup buckets + sample slice in the window inside one transaction. Returns immediately with `{ runId, status: "running" }`; poll `GET /runs/{id}` for completion. lastSyncedAt only advances forward, so a backfill never undoes incremental sync progress that ran ahead of it.',
10193
+ summary: "Reclassify historical traffic-source logs",
10194
+ description: 'Async one-shot backfill: pulls the last `days` of events (clamped server-side to the upstream retention ceiling \u2014 30d for Cloud Logging `_Default`; the WordPress plugin honours the same window via `since`/`until` query params), classifies them with the current rules, and replaces the hourly rollup buckets + sample slice in the window inside one transaction. Returns immediately with `{ runId, status: "running" }`; poll `GET /runs/{id}` for completion. lastSyncedAt only advances forward, so a backfill never undoes incremental sync progress that ran ahead of it. Supported source types: `cloud-run`, `wordpress`.',
10176
10195
  tags: ["traffic"],
10177
10196
  parameters: [
10178
10197
  nameParameter,
@@ -17411,6 +17430,7 @@ async function listWordpressTrafficEvents(options) {
17411
17430
  let cursor = options.cursor;
17412
17431
  let rawEntryCount = 0;
17413
17432
  let skippedEntryCount = 0;
17433
+ let hasMore = false;
17414
17434
  const events = [];
17415
17435
  for (let page = 0; page < maxPages; page += 1) {
17416
17436
  const url = new URL(endpoint);
@@ -17418,6 +17438,12 @@ async function listWordpressTrafficEvents(options) {
17418
17438
  if (cursor !== void 0 && cursor !== "") {
17419
17439
  url.searchParams.set("cursor", cursor);
17420
17440
  }
17441
+ if (options.since !== void 0 && options.since !== "") {
17442
+ url.searchParams.set("since", options.since);
17443
+ }
17444
+ if (options.until !== void 0 && options.until !== "") {
17445
+ url.searchParams.set("until", options.until);
17446
+ }
17421
17447
  const response = await fetch(url, {
17422
17448
  method: "GET",
17423
17449
  headers: {
@@ -17446,6 +17472,7 @@ async function listWordpressTrafficEvents(options) {
17446
17472
  }
17447
17473
  }
17448
17474
  cursor = body.next_cursor ?? void 0;
17475
+ hasMore = Boolean(body.has_more) && Boolean(cursor);
17449
17476
  if (!body.has_more || !cursor) break;
17450
17477
  }
17451
17478
  return {
@@ -17453,6 +17480,7 @@ async function listWordpressTrafficEvents(options) {
17453
17480
  rawEntryCount,
17454
17481
  skippedEntryCount,
17455
17482
  nextCursor: cursor,
17483
+ hasMore,
17456
17484
  endpoint
17457
17485
  };
17458
17486
  }
@@ -17462,6 +17490,8 @@ var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
17462
17490
  var DEFAULT_PAGE_SIZE3 = 1e3;
17463
17491
  var DEFAULT_MAX_PAGES3 = 5;
17464
17492
  var DEFAULT_SAMPLE_LIMIT2 = 100;
17493
+ var DEFAULT_WP_PAGE_SIZE = 500;
17494
+ var DEFAULT_WP_MAX_PAGES = 20;
17465
17495
  var MAX_TRACKED_EVENT_IDS = 1e3;
17466
17496
  var DEFAULT_BACKFILL_DAYS = 30;
17467
17497
  var MAX_BACKFILL_DAYS = 30;
@@ -17503,14 +17533,10 @@ async function runBackfillTask(options) {
17503
17533
  runId,
17504
17534
  project,
17505
17535
  sourceRow,
17506
- gcpProjectId,
17507
- serviceName,
17508
- location,
17509
- credential,
17510
17536
  windowStart,
17511
17537
  windowEnd,
17512
- pullEvents,
17513
- resolveAccessToken: resolveAccessToken2
17538
+ pullForBackfill,
17539
+ pullErrorPrefix
17514
17540
  } = options;
17515
17541
  const markFailed = (msg) => {
17516
17542
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -17522,33 +17548,11 @@ async function runBackfillTask(options) {
17522
17548
  } catch {
17523
17549
  }
17524
17550
  };
17525
- let accessToken;
17551
+ let allEvents;
17526
17552
  try {
17527
- accessToken = await resolveAccessToken2(credential);
17553
+ allEvents = await pullForBackfill();
17528
17554
  } catch (e) {
17529
- markFailed(`Failed to resolve Cloud Run access token: ${e instanceof Error ? e.message : String(e)}`);
17530
- return;
17531
- }
17532
- const allEvents = [];
17533
- try {
17534
- const page = await pullEvents(accessToken, {
17535
- gcpProjectId,
17536
- serviceName,
17537
- location,
17538
- startTime: windowStart.toISOString(),
17539
- endTime: windowEnd.toISOString(),
17540
- pageSize: DEFAULT_PAGE_SIZE3,
17541
- maxPages: BACKFILL_MAX_PAGES,
17542
- // Backfill is intentionally `firstSync: false`. We don't want desc
17543
- // ordering — the in-memory rollup builder handles any order, and the
17544
- // ring-buffer reseed at the end takes the most-recent IDs from the
17545
- // dedupedEvents anyway.
17546
- firstSync: false,
17547
- orderBy: "timestamp asc"
17548
- });
17549
- allEvents.push(...page.events);
17550
- } catch (e) {
17551
- markFailed(`Cloud Run pull failed: ${e instanceof Error ? e.message : String(e)}`);
17555
+ markFailed(`${pullErrorPrefix}: ${e instanceof Error ? e.message : String(e)}`);
17552
17556
  return;
17553
17557
  }
17554
17558
  if (allEvents.length === 0) {
@@ -17838,33 +17842,12 @@ async function trafficRoutes(app, opts) {
17838
17842
  if (!sourceRow || sourceRow.projectId !== project.id) {
17839
17843
  throw notFound("Traffic source", request.params.id);
17840
17844
  }
17841
- if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"]) {
17845
+ if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress) {
17842
17846
  throw validationError(
17843
- `Sync for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run is supported in v1.`
17847
+ `Sync for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run and wordpress are supported in v1.`
17844
17848
  );
17845
17849
  }
17846
- const credentialStore = opts.cloudRunCredentialStore;
17847
- if (!credentialStore) {
17848
- throw validationError("Cloud Run credential storage is not configured for this deployment");
17849
- }
17850
- const credential = credentialStore.getConnection(project.name);
17851
- if (!credential) {
17852
- throw validationError(
17853
- `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
17854
- );
17855
- }
17856
- const config = parseSourceConfig(sourceRow);
17857
- const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
17858
- const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
17859
- const location = config.location ?? credential.location ?? void 0;
17860
- const requestedMinutes = request.body?.sinceMinutes;
17861
- const windowMinutes = Number.isFinite(requestedMinutes) && requestedMinutes && requestedMinutes > 0 ? Math.floor(requestedMinutes) : syncWindowMinutes;
17862
17850
  const windowEnd = /* @__PURE__ */ new Date();
17863
- const requestedStartMs = windowEnd.getTime() - windowMinutes * 6e4;
17864
- const lastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
17865
- const windowStart = new Date(
17866
- Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
17867
- );
17868
17851
  const startedAt = windowEnd.toISOString();
17869
17852
  const syncStartedAtMs = windowEnd.getTime();
17870
17853
  const runId = crypto20.randomUUID();
@@ -17898,32 +17881,100 @@ async function trafficRoutes(app, opts) {
17898
17881
  } catch {
17899
17882
  }
17900
17883
  };
17901
- let accessToken;
17902
- try {
17903
- accessToken = await resolveAccessToken2(credential);
17904
- } catch (e) {
17905
- const msg = e instanceof Error ? e.message : String(e);
17906
- markFailed(msg, "PROVIDER_AUTH");
17907
- throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
17908
- }
17909
- const isFirstSync = !sourceRow.lastSyncedAt;
17910
- let allEvents = [];
17911
- try {
17912
- const page = await pullEvents(accessToken, {
17913
- gcpProjectId,
17914
- serviceName,
17915
- location,
17916
- startTime: windowStart.toISOString(),
17917
- endTime: windowEnd.toISOString(),
17918
- pageSize,
17919
- maxPages,
17920
- firstSync: isFirstSync
17921
- });
17922
- allEvents = page.events;
17923
- } catch (e) {
17924
- const msg = e instanceof Error ? e.message : String(e);
17925
- markFailed(msg, "PROVIDER_PULL");
17926
- throw providerError(`Cloud Run pull failed: ${msg}`);
17884
+ let windowStart;
17885
+ let allEvents;
17886
+ let nextCursor;
17887
+ let auditAction;
17888
+ if (sourceRow.sourceType === TrafficSourceTypes["cloud-run"]) {
17889
+ auditAction = "traffic.cloud-run.synced";
17890
+ const credentialStore = opts.cloudRunCredentialStore;
17891
+ if (!credentialStore) {
17892
+ throw validationError("Cloud Run credential storage is not configured for this deployment");
17893
+ }
17894
+ const credential = credentialStore.getConnection(project.name);
17895
+ if (!credential) {
17896
+ throw validationError(
17897
+ `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
17898
+ );
17899
+ }
17900
+ const config = parseSourceConfig(sourceRow);
17901
+ const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
17902
+ const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
17903
+ const location = config.location ?? credential.location ?? void 0;
17904
+ const requestedMinutes = request.body?.sinceMinutes;
17905
+ const windowMinutes = Number.isFinite(requestedMinutes) && requestedMinutes && requestedMinutes > 0 ? Math.floor(requestedMinutes) : syncWindowMinutes;
17906
+ const requestedStartMs = windowEnd.getTime() - windowMinutes * 6e4;
17907
+ const lastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
17908
+ windowStart = new Date(
17909
+ Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
17910
+ );
17911
+ let accessToken;
17912
+ try {
17913
+ accessToken = await resolveAccessToken2(credential);
17914
+ } catch (e) {
17915
+ const msg = e instanceof Error ? e.message : String(e);
17916
+ markFailed(msg, "PROVIDER_AUTH");
17917
+ throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
17918
+ }
17919
+ const isFirstSync = !sourceRow.lastSyncedAt;
17920
+ try {
17921
+ const page = await pullEvents(accessToken, {
17922
+ gcpProjectId,
17923
+ serviceName,
17924
+ location,
17925
+ startTime: windowStart.toISOString(),
17926
+ endTime: windowEnd.toISOString(),
17927
+ pageSize,
17928
+ maxPages,
17929
+ firstSync: isFirstSync
17930
+ });
17931
+ allEvents = page.events;
17932
+ } catch (e) {
17933
+ const msg = e instanceof Error ? e.message : String(e);
17934
+ markFailed(msg, "PROVIDER_PULL");
17935
+ throw providerError(`Cloud Run pull failed: ${msg}`);
17936
+ }
17937
+ } else {
17938
+ auditAction = "traffic.wordpress.synced";
17939
+ const credentialStore = opts.wordpressTrafficCredentialStore;
17940
+ if (!credentialStore) {
17941
+ throw validationError("WordPress traffic credential storage is not configured for this deployment");
17942
+ }
17943
+ const credential = credentialStore.getConnection(project.name);
17944
+ if (!credential) {
17945
+ app.db.delete(runs).where(eq23(runs.id, runId)).run();
17946
+ throw validationError(
17947
+ `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
17948
+ );
17949
+ }
17950
+ windowStart = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt) : windowEnd;
17951
+ const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
17952
+ const wpMaxPages = opts.defaultWordpressMaxPages ?? DEFAULT_WP_MAX_PAGES;
17953
+ const collected = [];
17954
+ let cursor = sourceRow.lastCursor ?? void 0;
17955
+ try {
17956
+ for (let page = 0; page < wpMaxPages; page += 1) {
17957
+ const pageResult = await pullWordpressEvents({
17958
+ baseUrl: credential.baseUrl,
17959
+ username: credential.username,
17960
+ applicationPassword: credential.applicationPassword,
17961
+ cursor,
17962
+ pageSize: wpPageSize,
17963
+ maxPages: 1
17964
+ });
17965
+ collected.push(...pageResult.events);
17966
+ const previousCursor = cursor;
17967
+ cursor = pageResult.nextCursor;
17968
+ if (!pageResult.hasMore) break;
17969
+ if (!cursor || cursor === previousCursor) break;
17970
+ }
17971
+ allEvents = collected;
17972
+ nextCursor = cursor;
17973
+ } catch (e) {
17974
+ const msg = e instanceof Error ? e.message : String(e);
17975
+ markFailed(msg, "PROVIDER_PULL");
17976
+ throw providerError(`WordPress pull failed: ${msg}`);
17977
+ }
17927
17978
  }
17928
17979
  let crawlerBucketRows = 0;
17929
17980
  let aiReferralBucketRows = 0;
@@ -18050,7 +18101,7 @@ async function trafficRoutes(app, opts) {
18050
18101
  }).run();
18051
18102
  sampleRows += 1;
18052
18103
  }
18053
- tx.update(trafficSources).set({
18104
+ const sourceUpdate = {
18054
18105
  status: TrafficSourceStatuses.connected,
18055
18106
  // Advance to windowEnd, not finishedAt — events arriving at the
18056
18107
  // source between windowEnd and finishedAt aren't in this pull's
@@ -18060,13 +18111,17 @@ async function trafficRoutes(app, opts) {
18060
18111
  lastError: null,
18061
18112
  lastEventIds: JSON.stringify(nextEventIds),
18062
18113
  updatedAt: finishedAt
18063
- }).where(eq23(trafficSources.id, sourceRow.id)).run();
18114
+ };
18115
+ if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
18116
+ sourceUpdate.lastCursor = nextCursor ?? null;
18117
+ }
18118
+ tx.update(trafficSources).set(sourceUpdate).where(eq23(trafficSources.id, sourceRow.id)).run();
18064
18119
  tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq23(runs.id, runId)).run();
18065
18120
  });
18066
18121
  writeAuditLog(app.db, {
18067
18122
  projectId: project.id,
18068
18123
  actor: "api",
18069
- action: "traffic.cloud-run.synced",
18124
+ action: auditAction,
18070
18125
  entityType: "traffic_source",
18071
18126
  entityId: sourceRow.id
18072
18127
  });
@@ -18104,19 +18159,9 @@ async function trafficRoutes(app, opts) {
18104
18159
  if (!sourceRow || sourceRow.projectId !== project.id) {
18105
18160
  throw notFound("Traffic source", request.params.id);
18106
18161
  }
18107
- if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"]) {
18162
+ if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress) {
18108
18163
  throw validationError(
18109
- `Backfill for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run is supported in v1.`
18110
- );
18111
- }
18112
- const credentialStore = opts.cloudRunCredentialStore;
18113
- if (!credentialStore) {
18114
- throw validationError("Cloud Run credential storage is not configured for this deployment");
18115
- }
18116
- const credential = credentialStore.getConnection(project.name);
18117
- if (!credential) {
18118
- throw validationError(
18119
- `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
18164
+ `Backfill for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run and wordpress are supported in v1.`
18120
18165
  );
18121
18166
  }
18122
18167
  const requestedDays = request.body?.days ?? DEFAULT_BACKFILL_DAYS;
@@ -18124,13 +18169,86 @@ async function trafficRoutes(app, opts) {
18124
18169
  throw validationError('"days" must be a positive integer');
18125
18170
  }
18126
18171
  const appliedDays = Math.min(requestedDays, MAX_BACKFILL_DAYS);
18127
- const config = parseSourceConfig(sourceRow);
18128
- const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
18129
- const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
18130
- const location = config.location ?? credential.location ?? void 0;
18131
18172
  const windowEnd = /* @__PURE__ */ new Date();
18132
18173
  const windowStart = new Date(windowEnd.getTime() - appliedDays * 864e5);
18133
18174
  windowStart.setUTCMinutes(0, 0, 0);
18175
+ let pullForBackfill;
18176
+ let pullErrorPrefix;
18177
+ if (sourceRow.sourceType === TrafficSourceTypes["cloud-run"]) {
18178
+ const credentialStore = opts.cloudRunCredentialStore;
18179
+ if (!credentialStore) {
18180
+ throw validationError("Cloud Run credential storage is not configured for this deployment");
18181
+ }
18182
+ const credential = credentialStore.getConnection(project.name);
18183
+ if (!credential) {
18184
+ throw validationError(
18185
+ `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
18186
+ );
18187
+ }
18188
+ const config = parseSourceConfig(sourceRow);
18189
+ const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
18190
+ const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
18191
+ const location = config.location ?? credential.location ?? void 0;
18192
+ pullErrorPrefix = "Cloud Run pull failed";
18193
+ pullForBackfill = async () => {
18194
+ const accessToken = await resolveAccessToken2(credential);
18195
+ const page = await pullEvents(accessToken, {
18196
+ gcpProjectId,
18197
+ serviceName,
18198
+ location,
18199
+ startTime: windowStart.toISOString(),
18200
+ endTime: windowEnd.toISOString(),
18201
+ pageSize: DEFAULT_PAGE_SIZE3,
18202
+ maxPages: BACKFILL_MAX_PAGES,
18203
+ // Backfill is intentionally `firstSync: false`. We don't want desc
18204
+ // ordering — the in-memory rollup builder handles any order, and the
18205
+ // ring-buffer reseed at the end takes the most-recent IDs from the
18206
+ // dedupedEvents anyway.
18207
+ firstSync: false,
18208
+ orderBy: "timestamp asc"
18209
+ });
18210
+ return page.events;
18211
+ };
18212
+ } else {
18213
+ const credentialStore = opts.wordpressTrafficCredentialStore;
18214
+ if (!credentialStore) {
18215
+ throw validationError("WordPress traffic credential storage is not configured for this deployment");
18216
+ }
18217
+ const credential = credentialStore.getConnection(project.name);
18218
+ if (!credential) {
18219
+ throw validationError(
18220
+ `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
18221
+ );
18222
+ }
18223
+ const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
18224
+ pullErrorPrefix = "WordPress pull failed";
18225
+ pullForBackfill = async () => {
18226
+ const collected = [];
18227
+ const windowStartIso = windowStart.toISOString();
18228
+ const windowEndIso = windowEnd.toISOString();
18229
+ let cursor = void 0;
18230
+ for (let page = 0; page < BACKFILL_MAX_PAGES; page += 1) {
18231
+ const pageResult = await pullWordpressEvents({
18232
+ baseUrl: credential.baseUrl,
18233
+ username: credential.username,
18234
+ applicationPassword: credential.applicationPassword,
18235
+ cursor,
18236
+ pageSize: wpPageSize,
18237
+ // Each call fetches a single page; the for-loop drives
18238
+ // continuation. Matches the WP sync path's pattern.
18239
+ maxPages: 1,
18240
+ since: windowStartIso,
18241
+ until: windowEndIso
18242
+ });
18243
+ collected.push(...pageResult.events);
18244
+ const previousCursor = cursor;
18245
+ cursor = pageResult.nextCursor;
18246
+ if (!pageResult.hasMore) break;
18247
+ if (!cursor || cursor === previousCursor) break;
18248
+ }
18249
+ return collected;
18250
+ };
18251
+ }
18134
18252
  const startedAt = windowEnd.toISOString();
18135
18253
  const runId = crypto20.randomUUID();
18136
18254
  app.db.insert(runs).values({
@@ -18148,15 +18266,10 @@ async function trafficRoutes(app, opts) {
18148
18266
  runId,
18149
18267
  project,
18150
18268
  sourceRow,
18151
- gcpProjectId,
18152
- serviceName,
18153
- location,
18154
- credential,
18155
18269
  windowStart,
18156
18270
  windowEnd,
18157
- appliedDays,
18158
- pullEvents,
18159
- resolveAccessToken: resolveAccessToken2
18271
+ pullForBackfill,
18272
+ pullErrorPrefix
18160
18273
  }).catch(() => {
18161
18274
  });
18162
18275
  const response = {
@@ -22887,7 +23000,8 @@ var JobRunner = class {
22887
23000
  throw new Error("No providers configured. Add at least one provider API key.");
22888
23001
  }
22889
23002
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
22890
- projectQueries = this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23003
+ const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23004
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray7(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
22891
23005
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
22892
23006
  const competitorDomains = projectCompetitors.map((c) => c.domain);
22893
23007
  const allDomains = effectiveDomains({
@@ -23167,7 +23281,8 @@ var JobRunner = class {
23167
23281
  status: runs.status,
23168
23282
  finishedAt: runs.finishedAt,
23169
23283
  error: runs.error,
23170
- trigger: runs.trigger
23284
+ trigger: runs.trigger,
23285
+ queries: runs.queries
23171
23286
  }).from(runs).where(eq27(runs.id, runId)).get();
23172
23287
  }
23173
23288
  isRunCancelled(runId) {
@@ -105,6 +105,7 @@ var runTriggerRequestSchema = z2.object({
105
105
  kind: z2.literal(RunKinds["answer-visibility"]).optional(),
106
106
  trigger: z2.literal(RunTriggers.manual).optional(),
107
107
  providers: z2.array(providerNameSchema).optional(),
108
+ queries: z2.array(z2.string().min(1)).min(1).optional(),
108
109
  location: z2.string().min(1).optional(),
109
110
  allLocations: z2.boolean().optional(),
110
111
  noLocation: z2.boolean().optional()
@@ -131,6 +132,7 @@ var runDtoSchema = z2.object({
131
132
  status: runStatusSchema,
132
133
  trigger: runTriggerSchema.default("manual"),
133
134
  location: z2.string().nullable().optional(),
135
+ queries: z2.array(z2.string()).nullable().optional(),
134
136
  startedAt: z2.string().nullable().optional(),
135
137
  finishedAt: z2.string().nullable().optional(),
136
138
  error: runErrorSchema.nullable().optional(),
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-CRQMGNPH.js";
11
+ } from "./chunk-HVW665A4.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
@@ -109,6 +109,7 @@ var runs = sqliteTable("runs", {
109
109
  status: text("status").notNull().default("queued"),
110
110
  trigger: text("trigger").notNull().default("manual"),
111
111
  location: text("location"),
112
+ queries: text("queries"),
112
113
  sourceId: text("source_id"),
113
114
  startedAt: text("started_at"),
114
115
  finishedAt: text("finished_at"),
@@ -1792,6 +1793,17 @@ var MIGRATION_VERSIONS = [
1792
1793
  `ALTER TABLE discovery_sessions ADD COLUMN run_id TEXT`,
1793
1794
  `CREATE INDEX IF NOT EXISTS idx_discovery_sessions_run ON discovery_sessions(run_id)`
1794
1795
  ]
1796
+ },
1797
+ {
1798
+ version: 57,
1799
+ name: "runs-scoped-queries",
1800
+ // Persists an optional subset of tracked queries to sweep on a per-run
1801
+ // basis. NULL = full sweep (the default and only behavior pre-v57); a JSON
1802
+ // array of query strings = scope. The job runner reads this to filter the
1803
+ // query fetch via `inArray`.
1804
+ statements: [
1805
+ `ALTER TABLE runs ADD COLUMN queries TEXT`
1806
+ ]
1795
1807
  }
1796
1808
  ];
1797
1809
  function isDuplicateColumnError(err) {