@ainyc/canonry 4.15.0 → 4.15.2

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.
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-C32VL5BB.js";
8
+ } from "./chunk-7SRKUAZO.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -66,7 +66,7 @@ import {
66
66
  schedules,
67
67
  trafficSources,
68
68
  usageCounters
69
- } from "./chunk-7HBZCGRL.js";
69
+ } from "./chunk-MI33SQL6.js";
70
70
  import {
71
71
  AGENT_MEMORY_VALUE_MAX_BYTES,
72
72
  AGENT_PROVIDER_IDS,
@@ -82,6 +82,7 @@ import {
82
82
  RunStatuses,
83
83
  RunTriggers,
84
84
  TrafficEventConfidences,
85
+ TrafficEventKinds,
85
86
  TrafficEvidenceKinds,
86
87
  TrafficSourceAuthModes,
87
88
  TrafficSourceStatuses,
@@ -152,7 +153,7 @@ import {
152
153
  visibilityStateFromAnswerMentioned,
153
154
  windowCutoff,
154
155
  wordpressEnvSchema
155
- } from "./chunk-6QTH5NS5.js";
156
+ } from "./chunk-ONI3TX2A.js";
156
157
 
157
158
  // src/telemetry.ts
158
159
  import crypto from "crypto";
@@ -9661,8 +9662,66 @@ var routeCatalog = [
9661
9662
  },
9662
9663
  responses: {
9663
9664
  200: { description: "Sync summary returned." },
9664
- 400: { description: "Invalid sync request, missing credentials, or upstream pull error." },
9665
- 404: { description: "Project or traffic source not found." }
9665
+ 400: { description: "Invalid sync request or missing credentials." },
9666
+ 404: { description: "Project or traffic source not found." },
9667
+ 502: { description: "Upstream Cloud Run pull or auth-token resolution failed." }
9668
+ }
9669
+ },
9670
+ {
9671
+ method: "get",
9672
+ path: "/api/v1/projects/{name}/traffic/sources",
9673
+ summary: "List non-archived traffic sources for a project",
9674
+ tags: ["traffic"],
9675
+ parameters: [nameParameter],
9676
+ responses: {
9677
+ 200: { description: "Source list returned." },
9678
+ 404: { description: "Project not found." }
9679
+ }
9680
+ },
9681
+ {
9682
+ method: "get",
9683
+ path: "/api/v1/projects/{name}/traffic/status",
9684
+ summary: "List non-archived traffic sources with last-24h totals and the latest sync run for each",
9685
+ description: "Single-call composite for the `canonry traffic status` view: same shape as `GET /traffic/sources/{id}` but returned as `{ sources: TrafficSourceDetailDto[] }` for every non-archived source. Lets agents and the dashboard avoid an N+1 fan-out.",
9686
+ tags: ["traffic"],
9687
+ parameters: [nameParameter],
9688
+ responses: {
9689
+ 200: { description: "Status returned." },
9690
+ 404: { description: "Project not found." }
9691
+ }
9692
+ },
9693
+ {
9694
+ method: "get",
9695
+ path: "/api/v1/projects/{name}/traffic/sources/{id}",
9696
+ summary: "Get a single traffic source with last-24h totals and the latest sync run",
9697
+ tags: ["traffic"],
9698
+ parameters: [
9699
+ nameParameter,
9700
+ { name: "id", in: "path", required: true, description: "Traffic source ID.", schema: stringSchema }
9701
+ ],
9702
+ responses: {
9703
+ 200: { description: "Source detail returned." },
9704
+ 404: { description: "Project or source not found." }
9705
+ }
9706
+ },
9707
+ {
9708
+ method: "get",
9709
+ path: "/api/v1/projects/{name}/traffic/events",
9710
+ summary: "List rolled-up crawler and AI-referral hits within a window",
9711
+ description: "Returns hourly rollup rows from `crawler_events_hourly` and `ai_referral_events_hourly`. Defaults to the last 24h. Totals reflect the full window; the `events` array is capped by `limit` (default 500, max 5000).",
9712
+ tags: ["traffic"],
9713
+ parameters: [
9714
+ nameParameter,
9715
+ { name: "since", in: "query", description: "ISO-8601 window start (defaults to 24h ago).", schema: stringSchema },
9716
+ { name: "until", in: "query", description: "ISO-8601 window end (defaults to now).", schema: stringSchema },
9717
+ { name: "kind", in: "query", description: 'Filter to "crawler", "ai-referral", or "all" (default).', schema: stringSchema },
9718
+ { name: "limit", in: "query", description: "Max rows per kind in the events array (default 500, max 5000).", schema: stringSchema },
9719
+ { name: "sourceId", in: "query", description: "Restrict to a single traffic source.", schema: stringSchema }
9720
+ ],
9721
+ responses: {
9722
+ 200: { description: "Events returned with windowed totals." },
9723
+ 400: { description: "Invalid query parameters." },
9724
+ 404: { description: "Project not found." }
9666
9725
  }
9667
9726
  }
9668
9727
  ];
@@ -15893,7 +15952,7 @@ async function backlinksRoutes(app, opts) {
15893
15952
 
15894
15953
  // ../api-routes/src/traffic.ts
15895
15954
  import crypto20 from "crypto";
15896
- import { eq as eq23, sql as sql7 } from "drizzle-orm";
15955
+ import { and as and12, desc as desc12, eq as eq23, gte, lte, sql as sql7 } from "drizzle-orm";
15897
15956
 
15898
15957
  // ../integration-cloud-run/src/auth.ts
15899
15958
  import crypto19 from "crypto";
@@ -16500,10 +16559,11 @@ function incrementBucket(map, key, fields) {
16500
16559
  }
16501
16560
 
16502
16561
  // ../api-routes/src/traffic.ts
16503
- var DEFAULT_SYNC_WINDOW_MINUTES = 60;
16562
+ var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
16504
16563
  var DEFAULT_PAGE_SIZE2 = 1e3;
16505
16564
  var DEFAULT_MAX_PAGES2 = 5;
16506
16565
  var DEFAULT_SAMPLE_LIMIT2 = 100;
16566
+ var MAX_TRACKED_EVENT_IDS = 1e3;
16507
16567
  function parseSourceConfig(row) {
16508
16568
  return parseJsonColumn(row.configJson, {});
16509
16569
  }
@@ -16664,17 +16724,24 @@ async function trafficRoutes(app, opts) {
16664
16724
  kind: RunKinds["traffic-sync"],
16665
16725
  status: RunStatuses.running,
16666
16726
  trigger: RunTriggers.manual,
16727
+ sourceId: sourceRow.id,
16667
16728
  startedAt,
16668
16729
  createdAt: startedAt
16669
16730
  }).run();
16731
+ const markFailed = (msg) => {
16732
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
16733
+ app.db.transaction((tx) => {
16734
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq23(runs.id, runId)).run();
16735
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq23(trafficSources.id, sourceRow.id)).run();
16736
+ });
16737
+ };
16670
16738
  let accessToken;
16671
16739
  try {
16672
16740
  accessToken = await resolveAccessToken2(credential);
16673
16741
  } catch (e) {
16674
16742
  const msg = e instanceof Error ? e.message : String(e);
16675
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
16676
- app.db.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(trafficSources.id, sourceRow.id)).run();
16677
- throw validationError(`Failed to resolve Cloud Run access token: ${msg}`);
16743
+ markFailed(msg);
16744
+ throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
16678
16745
  }
16679
16746
  let allEvents = [];
16680
16747
  try {
@@ -16690,11 +16757,23 @@ async function trafficRoutes(app, opts) {
16690
16757
  allEvents = page.events;
16691
16758
  } catch (e) {
16692
16759
  const msg = e instanceof Error ? e.message : String(e);
16693
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
16694
- app.db.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(trafficSources.id, sourceRow.id)).run();
16695
- throw validationError(`Cloud Run pull failed: ${msg}`);
16696
- }
16697
- const report = buildTrafficProbeReport(allEvents, { sampleLimit });
16760
+ markFailed(msg);
16761
+ throw providerError(`Cloud Run pull failed: ${msg}`);
16762
+ }
16763
+ const seenEventIds = new Set(parseJsonColumn(sourceRow.lastEventIds, []));
16764
+ const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
16765
+ const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
16766
+ const previousIds = parseJsonColumn(sourceRow.lastEventIds, []);
16767
+ const merged = [];
16768
+ const mergedSet = /* @__PURE__ */ new Set();
16769
+ for (const id of [...newSorted, ...previousIds]) {
16770
+ if (mergedSet.has(id)) continue;
16771
+ mergedSet.add(id);
16772
+ merged.push(id);
16773
+ if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
16774
+ }
16775
+ const nextEventIds = merged;
16776
+ const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
16698
16777
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
16699
16778
  let crawlerBucketRows = 0;
16700
16779
  let aiReferralBucketRows = 0;
@@ -16800,6 +16879,7 @@ async function trafficRoutes(app, opts) {
16800
16879
  status: TrafficSourceStatuses.connected,
16801
16880
  lastSyncedAt: finishedAt,
16802
16881
  lastError: null,
16882
+ lastEventIds: JSON.stringify(nextEventIds),
16803
16883
  updatedAt: finishedAt
16804
16884
  }).where(eq23(trafficSources.id, sourceRow.id)).run();
16805
16885
  tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq23(runs.id, runId)).run();
@@ -16827,6 +16907,177 @@ async function trafficRoutes(app, opts) {
16827
16907
  };
16828
16908
  return response;
16829
16909
  });
16910
+ function buildSourceDetail(projectId, row, since) {
16911
+ const crawlerTotals = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
16912
+ and12(
16913
+ eq23(crawlerEventsHourly.sourceId, row.id),
16914
+ gte(crawlerEventsHourly.tsHour, since)
16915
+ )
16916
+ ).get();
16917
+ const aiTotals = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
16918
+ and12(
16919
+ eq23(aiReferralEventsHourly.sourceId, row.id),
16920
+ gte(aiReferralEventsHourly.tsHour, since)
16921
+ )
16922
+ ).get();
16923
+ const sampleTotals = app.db.select({ total: sql7`COUNT(*)` }).from(rawEventSamples).where(
16924
+ and12(
16925
+ eq23(rawEventSamples.sourceId, row.id),
16926
+ gte(rawEventSamples.ts, since)
16927
+ )
16928
+ ).get();
16929
+ const latestRun = app.db.select().from(runs).where(
16930
+ and12(
16931
+ eq23(runs.projectId, projectId),
16932
+ eq23(runs.kind, RunKinds["traffic-sync"]),
16933
+ eq23(runs.sourceId, row.id)
16934
+ )
16935
+ ).orderBy(desc12(runs.startedAt)).limit(1).get();
16936
+ return {
16937
+ ...rowToDto(row),
16938
+ totals24h: {
16939
+ crawlerHits: Number(crawlerTotals?.total ?? 0),
16940
+ aiReferralHits: Number(aiTotals?.total ?? 0),
16941
+ sampleCount: Number(sampleTotals?.total ?? 0)
16942
+ },
16943
+ latestRun: latestRun ? {
16944
+ runId: latestRun.id,
16945
+ status: latestRun.status,
16946
+ startedAt: latestRun.startedAt,
16947
+ finishedAt: latestRun.finishedAt ?? null,
16948
+ error: latestRun.error ?? null
16949
+ } : null
16950
+ };
16951
+ }
16952
+ app.get("/projects/:name/traffic/sources", async (request) => {
16953
+ const project = resolveProject(app.db, request.params.name);
16954
+ const rows = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).orderBy(desc12(trafficSources.createdAt)).all();
16955
+ const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
16956
+ const response = { sources };
16957
+ return response;
16958
+ });
16959
+ app.get("/projects/:name/traffic/status", async (request) => {
16960
+ const project = resolveProject(app.db, request.params.name);
16961
+ const rows = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).orderBy(desc12(trafficSources.createdAt)).all();
16962
+ const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
16963
+ const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
16964
+ const response = { sources };
16965
+ return response;
16966
+ });
16967
+ app.get(
16968
+ "/projects/:name/traffic/sources/:id",
16969
+ async (request) => {
16970
+ const project = resolveProject(app.db, request.params.name);
16971
+ const row = app.db.select().from(trafficSources).where(eq23(trafficSources.id, request.params.id)).get();
16972
+ if (!row || row.projectId !== project.id) {
16973
+ throw notFound("Traffic source", request.params.id);
16974
+ }
16975
+ const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
16976
+ return buildSourceDetail(project.id, row, since);
16977
+ }
16978
+ );
16979
+ app.get("/projects/:name/traffic/events", async (request) => {
16980
+ const project = resolveProject(app.db, request.params.name);
16981
+ const now = /* @__PURE__ */ new Date();
16982
+ const defaultSince = new Date(now.getTime() - 24 * 60 * 6e4);
16983
+ const sinceParam = request.query?.since;
16984
+ const untilParam = request.query?.until;
16985
+ const since = sinceParam ? new Date(sinceParam) : defaultSince;
16986
+ const until = untilParam ? new Date(untilParam) : now;
16987
+ if (Number.isNaN(since.getTime())) {
16988
+ throw validationError('"since" must be an ISO-8601 timestamp');
16989
+ }
16990
+ if (Number.isNaN(until.getTime())) {
16991
+ throw validationError('"until" must be an ISO-8601 timestamp');
16992
+ }
16993
+ if (since.getTime() > until.getTime()) {
16994
+ throw validationError('"since" must be \u2264 "until"');
16995
+ }
16996
+ const kindParam = request.query?.kind;
16997
+ let kind = "all";
16998
+ if (kindParam !== void 0) {
16999
+ if (kindParam === "all" || kindParam === TrafficEventKinds.crawler || kindParam === TrafficEventKinds["ai-referral"]) {
17000
+ kind = kindParam;
17001
+ } else {
17002
+ throw validationError(`"kind" must be one of: all, ${TrafficEventKinds.crawler}, ${TrafficEventKinds["ai-referral"]}`);
17003
+ }
17004
+ }
17005
+ const limitParam = request.query?.limit;
17006
+ const requestedLimit = limitParam ? parseInt(limitParam, 10) : 500;
17007
+ if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) {
17008
+ throw validationError('"limit" must be a positive integer');
17009
+ }
17010
+ const limit = Math.min(requestedLimit, 5e3);
17011
+ const sourceIdParam = request.query?.sourceId;
17012
+ const sinceIso = since.toISOString();
17013
+ const untilIso = until.toISOString();
17014
+ const events = [];
17015
+ let crawlerTotal = 0;
17016
+ let aiReferralTotal = 0;
17017
+ if (kind === "all" || kind === TrafficEventKinds.crawler) {
17018
+ const crawlerFilters = [
17019
+ eq23(crawlerEventsHourly.projectId, project.id),
17020
+ gte(crawlerEventsHourly.tsHour, sinceIso),
17021
+ lte(crawlerEventsHourly.tsHour, untilIso)
17022
+ ];
17023
+ if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
17024
+ const crawlerWhere = and12(...crawlerFilters);
17025
+ const total = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
17026
+ crawlerTotal = Number(total?.total ?? 0);
17027
+ const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
17028
+ for (const r of rows) {
17029
+ events.push({
17030
+ kind: TrafficEventKinds.crawler,
17031
+ sourceId: r.sourceId,
17032
+ tsHour: r.tsHour,
17033
+ botId: r.botId,
17034
+ operator: r.operator,
17035
+ verificationStatus: r.verificationStatus,
17036
+ pathNormalized: r.pathNormalized,
17037
+ status: r.status,
17038
+ hits: r.hits
17039
+ });
17040
+ }
17041
+ }
17042
+ if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
17043
+ const aiFilters = [
17044
+ eq23(aiReferralEventsHourly.projectId, project.id),
17045
+ gte(aiReferralEventsHourly.tsHour, sinceIso),
17046
+ lte(aiReferralEventsHourly.tsHour, untilIso)
17047
+ ];
17048
+ if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
17049
+ const aiWhere = and12(...aiFilters);
17050
+ const total = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
17051
+ aiReferralTotal = Number(total?.total ?? 0);
17052
+ const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
17053
+ for (const r of rows) {
17054
+ events.push({
17055
+ kind: TrafficEventKinds["ai-referral"],
17056
+ sourceId: r.sourceId,
17057
+ tsHour: r.tsHour,
17058
+ product: r.product,
17059
+ operator: r.operator,
17060
+ sourceDomain: r.sourceDomain,
17061
+ evidenceType: r.evidenceType,
17062
+ landingPathNormalized: r.landingPathNormalized,
17063
+ status: r.status,
17064
+ hits: r.sessionsOrHits
17065
+ });
17066
+ }
17067
+ }
17068
+ events.sort((a, b) => a.tsHour < b.tsHour ? 1 : a.tsHour > b.tsHour ? -1 : 0);
17069
+ const trimmed = events.slice(0, limit);
17070
+ const response = {
17071
+ windowStart: sinceIso,
17072
+ windowEnd: untilIso,
17073
+ totals: {
17074
+ crawlerHits: crawlerTotal,
17075
+ aiReferralHits: aiReferralTotal
17076
+ },
17077
+ events: trimmed
17078
+ };
17079
+ return response;
17080
+ });
16830
17081
  }
16831
17082
 
16832
17083
  // ../api-routes/src/doctor/checks/bing-auth.ts
@@ -20198,7 +20449,7 @@ import crypto22 from "crypto";
20198
20449
  import fs7 from "fs";
20199
20450
  import path9 from "path";
20200
20451
  import os5 from "os";
20201
- import { and as and12, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
20452
+ import { and as and13, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
20202
20453
 
20203
20454
  // src/run-telemetry.ts
20204
20455
  import crypto21 from "crypto";
@@ -20577,7 +20828,7 @@ var JobRunner = class {
20577
20828
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
20578
20829
  }
20579
20830
  if (existingRun.status === "queued") {
20580
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and12(eq24(runs.id, runId), eq24(runs.status, "queued"))).run();
20831
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and13(eq24(runs.id, runId), eq24(runs.status, "queued"))).run();
20581
20832
  }
20582
20833
  this.throwIfRunCancelled(runId);
20583
20834
  const project = this.db.select().from(projects).where(eq24(projects.id, projectId)).get();
@@ -20936,7 +21187,7 @@ function buildPhases(input) {
20936
21187
 
20937
21188
  // src/gsc-sync.ts
20938
21189
  import crypto23 from "crypto";
20939
- import { eq as eq25, and as and13, sql as sql9 } from "drizzle-orm";
21190
+ import { eq as eq25, and as and14, sql as sql9 } from "drizzle-orm";
20940
21191
  var log2 = createLogger("GscSync");
20941
21192
  function formatDate3(d) {
20942
21193
  return d.toISOString().split("T")[0];
@@ -20988,7 +21239,7 @@ async function executeGscSync(db, runId, projectId, opts) {
20988
21239
  });
20989
21240
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
20990
21241
  db.delete(gscSearchData).where(
20991
- and13(
21242
+ and14(
20992
21243
  eq25(gscSearchData.projectId, projectId),
20993
21244
  sql9`${gscSearchData.date} >= ${startDate}`,
20994
21245
  sql9`${gscSearchData.date} <= ${endDate}`
@@ -21077,7 +21328,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21077
21328
  }
21078
21329
  }
21079
21330
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
21080
- db.delete(gscCoverageSnapshots).where(and13(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
21331
+ db.delete(gscCoverageSnapshots).where(and14(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
21081
21332
  db.insert(gscCoverageSnapshots).values({
21082
21333
  id: crypto23.randomUUID(),
21083
21334
  projectId,
@@ -21100,7 +21351,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21100
21351
 
21101
21352
  // src/gsc-inspect-sitemap.ts
21102
21353
  import crypto24 from "crypto";
21103
- import { eq as eq26, and as and14 } from "drizzle-orm";
21354
+ import { eq as eq26, and as and15 } from "drizzle-orm";
21104
21355
 
21105
21356
  // src/sitemap-parser.ts
21106
21357
  var log3 = createLogger("SitemapParser");
@@ -21316,7 +21567,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21316
21567
  }
21317
21568
  }
21318
21569
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
21319
- db.delete(gscCoverageSnapshots).where(and14(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
21570
+ db.delete(gscCoverageSnapshots).where(and15(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
21320
21571
  db.insert(gscCoverageSnapshots).values({
21321
21572
  id: crypto24.randomUUID(),
21322
21573
  projectId,
@@ -21340,7 +21591,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21340
21591
 
21341
21592
  // src/bing-inspect-sitemap.ts
21342
21593
  import crypto25 from "crypto";
21343
- import { eq as eq27, desc as desc12 } from "drizzle-orm";
21594
+ import { eq as eq27, desc as desc13 } from "drizzle-orm";
21344
21595
  var log5 = createLogger("BingInspectSitemap");
21345
21596
  function parseBingDate2(value) {
21346
21597
  if (!value) return null;
@@ -21461,7 +21712,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21461
21712
  await new Promise((r) => setTimeout(r, 1e3));
21462
21713
  }
21463
21714
  }
21464
- const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
21715
+ const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(desc13(bingUrlInspections.inspectedAt)).all();
21465
21716
  const latestByUrl = /* @__PURE__ */ new Map();
21466
21717
  const definitiveByUrl = /* @__PURE__ */ new Map();
21467
21718
  for (const row of allInspections) {
@@ -21527,7 +21778,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21527
21778
  // src/commoncrawl-sync.ts
21528
21779
  import crypto26 from "crypto";
21529
21780
  import path10 from "path";
21530
- import { and as and15, eq as eq28, sql as sql10 } from "drizzle-orm";
21781
+ import { and as and16, eq as eq28, sql as sql10 } from "drizzle-orm";
21531
21782
  var log6 = createLogger("CommonCrawlSync");
21532
21783
  var INSERT_CHUNK_SIZE = 1e4;
21533
21784
  function defaultDeps() {
@@ -21718,7 +21969,7 @@ function computeSummary(rows) {
21718
21969
  // src/backlink-extract.ts
21719
21970
  import crypto27 from "crypto";
21720
21971
  import fs8 from "fs";
21721
- import { and as and16, desc as desc13, eq as eq29 } from "drizzle-orm";
21972
+ import { and as and17, desc as desc14, eq as eq29 } from "drizzle-orm";
21722
21973
  var log7 = createLogger("BacklinkExtract");
21723
21974
  function defaultDeps2() {
21724
21975
  return {
@@ -21736,7 +21987,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
21736
21987
  if (!project) {
21737
21988
  throw new Error(`Project not found: ${projectId}`);
21738
21989
  }
21739
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc13(ccReleaseSyncs.createdAt)).limit(1).get();
21990
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc14(ccReleaseSyncs.createdAt)).limit(1).get();
21740
21991
  if (!sync) {
21741
21992
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
21742
21993
  }
@@ -21764,7 +22015,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
21764
22015
  const targetDomain = project.canonicalDomain;
21765
22016
  db.transaction((tx) => {
21766
22017
  tx.delete(backlinkDomains).where(
21767
- and16(eq29(backlinkDomains.projectId, projectId), eq29(backlinkDomains.release, release))
22018
+ and17(eq29(backlinkDomains.projectId, projectId), eq29(backlinkDomains.release, release))
21768
22019
  ).run();
21769
22020
  if (rows.length > 0) {
21770
22021
  const values = rows.map((r) => ({
@@ -22020,7 +22271,7 @@ var Scheduler = class {
22020
22271
  };
22021
22272
 
22022
22273
  // src/notifier.ts
22023
- import { eq as eq31, desc as desc14, and as and17, or as or4 } from "drizzle-orm";
22274
+ import { eq as eq31, desc as desc15, and as and18, or as or4 } from "drizzle-orm";
22024
22275
  import crypto28 from "crypto";
22025
22276
  var log9 = createLogger("Notifier");
22026
22277
  var Notifier = class {
@@ -22126,11 +22377,11 @@ var Notifier = class {
22126
22377
  }
22127
22378
  computeTransitions(runId, projectId) {
22128
22379
  const recentRuns = this.db.select().from(runs).where(
22129
- and17(
22380
+ and18(
22130
22381
  eq31(runs.projectId, projectId),
22131
22382
  or4(eq31(runs.status, "completed"), eq31(runs.status, "partial"))
22132
22383
  )
22133
- ).orderBy(desc14(runs.createdAt)).limit(2).all();
22384
+ ).orderBy(desc15(runs.createdAt)).limit(2).all();
22134
22385
  if (recentRuns.length < 2) return [];
22135
22386
  const currentRunId = recentRuns[0].id;
22136
22387
  const previousRunId = recentRuns[1].id;
@@ -22612,7 +22863,7 @@ function resolveSessionProviderAndModel(config, opts) {
22612
22863
 
22613
22864
  // src/agent/memory-store.ts
22614
22865
  import crypto29 from "crypto";
22615
- import { and as and18, desc as desc15, eq as eq32, like as like2, sql as sql11 } from "drizzle-orm";
22866
+ import { and as and19, desc as desc16, eq as eq32, like as like2, sql as sql11 } from "drizzle-orm";
22616
22867
  var COMPACTION_KEY_PREFIX = "compaction:";
22617
22868
  var COMPACTION_NOTES_PER_SESSION = 3;
22618
22869
  function rowToDto2(row) {
@@ -22626,7 +22877,7 @@ function rowToDto2(row) {
22626
22877
  };
22627
22878
  }
22628
22879
  function listMemoryEntries(db, projectId, opts = {}) {
22629
- const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(desc15(agentMemory.updatedAt));
22880
+ const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(desc16(agentMemory.updatedAt));
22630
22881
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
22631
22882
  return rows.map(rowToDto2);
22632
22883
  }
@@ -22657,12 +22908,12 @@ function upsertMemoryEntry(db, args) {
22657
22908
  updatedAt: now
22658
22909
  }
22659
22910
  }).run();
22660
- const row = db.select().from(agentMemory).where(and18(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, args.key))).get();
22911
+ const row = db.select().from(agentMemory).where(and19(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, args.key))).get();
22661
22912
  if (!row) throw new Error("memory upsert produced no row");
22662
22913
  return rowToDto2(row);
22663
22914
  }
22664
22915
  function deleteMemoryEntry(db, projectId, key) {
22665
- const result = db.delete(agentMemory).where(and18(eq32(agentMemory.projectId, projectId), eq32(agentMemory.key, key))).run();
22916
+ const result = db.delete(agentMemory).where(and19(eq32(agentMemory.projectId, projectId), eq32(agentMemory.key, key))).run();
22666
22917
  const changes = result.changes ?? 0;
22667
22918
  return changes > 0;
22668
22919
  }
@@ -22691,16 +22942,16 @@ function writeCompactionNote(db, args) {
22691
22942
  }).run();
22692
22943
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
22693
22944
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
22694
- and18(
22945
+ and19(
22695
22946
  eq32(agentMemory.projectId, args.projectId),
22696
22947
  like2(agentMemory.key, `${sessionPrefix}%`)
22697
22948
  )
22698
- ).orderBy(desc15(agentMemory.updatedAt)).all();
22949
+ ).orderBy(desc16(agentMemory.updatedAt)).all();
22699
22950
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
22700
22951
  if (stale.length > 0) {
22701
22952
  tx.delete(agentMemory).where(sql11`${agentMemory.id} IN (${sql11.join(stale.map((s) => sql11`${s}`), sql11`, `)})`).run();
22702
22953
  }
22703
- const row = tx.select().from(agentMemory).where(and18(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, key))).get();
22954
+ const row = tx.select().from(agentMemory).where(and19(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, key))).get();
22704
22955
  if (row) inserted = rowToDto2(row);
22705
22956
  });
22706
22957
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-6QTH5NS5.js";
11
+ } from "./chunk-ONI3TX2A.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
@@ -104,13 +104,15 @@ var runs = sqliteTable("runs", {
104
104
  status: text("status").notNull().default("queued"),
105
105
  trigger: text("trigger").notNull().default("manual"),
106
106
  location: text("location"),
107
+ sourceId: text("source_id"),
107
108
  startedAt: text("started_at"),
108
109
  finishedAt: text("finished_at"),
109
110
  error: text("error"),
110
111
  createdAt: text("created_at").notNull()
111
112
  }, (table) => [
112
113
  index("idx_runs_project").on(table.projectId),
113
- index("idx_runs_status").on(table.status)
114
+ index("idx_runs_status").on(table.status),
115
+ index("idx_runs_source").on(table.sourceId)
114
116
  ]);
115
117
  var querySnapshots = sqliteTable("query_snapshots", {
116
118
  id: text("id").primaryKey(),
@@ -555,6 +557,11 @@ var trafficSources = sqliteTable("traffic_sources", {
555
557
  lastSyncedAt: text("last_synced_at"),
556
558
  lastCursor: text("last_cursor"),
557
559
  lastError: text("last_error"),
560
+ // JSON-encoded array of normalized event IDs (e.g. `cloud-run:<ts>:<insertId>`)
561
+ // observed in the most recent successful sync. Bounded ring buffer used to
562
+ // dedupe across sync runs at the boundary timestamp where lastSyncedAt
563
+ // clamping alone leaves a small overlap window.
564
+ lastEventIds: text("last_event_ids"),
558
565
  archivedAt: text("archived_at"),
559
566
  configJson: text("config_json").notNull().default("{}"),
560
567
  createdAt: text("created_at").notNull(),
@@ -1586,6 +1593,24 @@ var MIGRATION_VERSIONS = [
1586
1593
  tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
1587
1594
  ON ga_ai_referrals(project_id, date, source, medium, source_dimension, channel_group, landing_page)`));
1588
1595
  }
1596
+ },
1597
+ {
1598
+ version: 51,
1599
+ name: "runs-source-id",
1600
+ statements: [
1601
+ `ALTER TABLE runs ADD COLUMN source_id TEXT`,
1602
+ `CREATE INDEX IF NOT EXISTS idx_runs_source ON runs(source_id)`
1603
+ ]
1604
+ },
1605
+ {
1606
+ version: 52,
1607
+ name: "traffic-sources-last-event-ids",
1608
+ statements: [
1609
+ // JSON-encoded array of normalized event IDs from the previous sync,
1610
+ // used for cross-sync boundary-window dedupe so a longer default
1611
+ // sync window (or any overlapping re-sync) cannot double-count.
1612
+ `ALTER TABLE traffic_sources ADD COLUMN last_event_ids TEXT`
1613
+ ]
1589
1614
  }
1590
1615
  ];
1591
1616
  function isDuplicateColumnError(err) {