@ainyc/canonry 4.33.0 → 4.34.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.
@@ -123,7 +123,14 @@ var runs = sqliteTable("runs", {
123
123
  var querySnapshots = sqliteTable("query_snapshots", {
124
124
  id: text("id").primaryKey(),
125
125
  runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
126
- queryId: text("query_id").notNull().references(() => queries.id, { onDelete: "cascade" }),
126
+ // `query_id` is nullable + `ON DELETE SET NULL` so historical snapshots
127
+ // outlive their queries row. Pre-v58 this FK cascaded — deleting a tracked
128
+ // query (PUT /queries replace, individual delete, `canonry apply` dropping
129
+ // one) silently wiped the entire citation history for that query. With SET
130
+ // NULL the snapshot survives; `queryText` keeps it self-describing when
131
+ // the queries row is gone.
132
+ queryId: text("query_id").references(() => queries.id, { onDelete: "set null" }),
133
+ queryText: text("query_text"),
127
134
  provider: text("provider").notNull().default("gemini"),
128
135
  model: text("model"),
129
136
  citationState: text("citation_state").notNull(),
@@ -1804,6 +1811,80 @@ var MIGRATION_VERSIONS = [
1804
1811
  statements: [
1805
1812
  `ALTER TABLE runs ADD COLUMN queries TEXT`
1806
1813
  ]
1814
+ },
1815
+ {
1816
+ version: 58,
1817
+ name: "snapshots-preserve-on-query-delete",
1818
+ // The legacy `query_snapshots.query_id` FK was `ON DELETE CASCADE`, so a
1819
+ // routine basket edit (PUT /queries replace, individual delete, `canonry
1820
+ // apply` dropping a query) silently destroyed every historical citation
1821
+ // snapshot for the removed queries — the regression history, transitions,
1822
+ // and competitor-overlap evidence that are canonry's whole value.
1823
+ //
1824
+ // Fix: rebuild `query_snapshots` with `query_id` nullable + `ON DELETE
1825
+ // SET NULL`, and add a denormalized `query_text` column populated from
1826
+ // `queries.query` via the join. SQLite can't change FK or NOT NULL in
1827
+ // place — same canonical table-rebuild pattern v53 used. All statements
1828
+ // run inside the migration runner's single transaction.
1829
+ //
1830
+ // `run_id` keeps `ON DELETE CASCADE` — deleting a run legitimately
1831
+ // removes its snapshots. Indexes are recreated on the renamed table.
1832
+ statements: [
1833
+ `CREATE TABLE IF NOT EXISTS query_snapshots_v58 (
1834
+ id TEXT PRIMARY KEY,
1835
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
1836
+ query_id TEXT REFERENCES queries(id) ON DELETE SET NULL,
1837
+ query_text TEXT,
1838
+ provider TEXT NOT NULL DEFAULT 'gemini',
1839
+ model TEXT,
1840
+ citation_state TEXT NOT NULL,
1841
+ answer_mentioned INTEGER,
1842
+ answer_text TEXT,
1843
+ cited_domains TEXT NOT NULL DEFAULT '[]',
1844
+ competitor_overlap TEXT NOT NULL DEFAULT '[]',
1845
+ recommended_competitors TEXT NOT NULL DEFAULT '[]',
1846
+ location TEXT,
1847
+ screenshot_path TEXT,
1848
+ raw_response TEXT,
1849
+ created_at TEXT NOT NULL
1850
+ )`,
1851
+ // Backfill `query_text` from joined queries.query so existing snapshots
1852
+ // stay readable even if their query is later deleted.
1853
+ //
1854
+ // IMPORTANT: we use `q.id` (the JOINED queries.id), not `qs.query_id`.
1855
+ // Production DBs may already contain snapshots whose `qs.query_id`
1856
+ // dangles — a queries row was hard-deleted at some point without
1857
+ // cascading (PRAGMA foreign_keys was OFF, or pre-FK schema). Copying
1858
+ // `qs.query_id` directly would re-introduce those dangling refs into
1859
+ // the new table, which now validates them at INSERT (the new FK still
1860
+ // requires query_id values to match queries.id when non-null). Reading
1861
+ // through the LEFT JOIN forces every value to be either a valid `q.id`
1862
+ // or NULL — pre-existing orphans land with NULL `query_id` / NULL
1863
+ // `query_text`, preserving the snapshot row instead of failing the
1864
+ // migration. The May 2026 azcoatings DB had 459 such pre-existing
1865
+ // orphans; without this guard, migrate() throws SQLITE_CONSTRAINT_FOREIGNKEY.
1866
+ `INSERT INTO query_snapshots_v58 (
1867
+ id, run_id, query_id, query_text, provider, model, citation_state,
1868
+ answer_mentioned, answer_text, cited_domains, competitor_overlap,
1869
+ recommended_competitors, location, screenshot_path, raw_response,
1870
+ created_at
1871
+ )
1872
+ SELECT qs.id, qs.run_id, q.id, q.query, qs.provider, qs.model,
1873
+ qs.citation_state, qs.answer_mentioned, qs.answer_text,
1874
+ qs.cited_domains, qs.competitor_overlap, qs.recommended_competitors,
1875
+ qs.location, qs.screenshot_path, qs.raw_response, qs.created_at
1876
+ FROM query_snapshots qs
1877
+ LEFT JOIN queries q ON q.id = qs.query_id`,
1878
+ `DROP TABLE query_snapshots`,
1879
+ `ALTER TABLE query_snapshots_v58 RENAME TO query_snapshots`,
1880
+ // Recreate the indexes that didn't survive the rename.
1881
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_run ON query_snapshots(run_id)`,
1882
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_query ON query_snapshots(query_id)`,
1883
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_citation_state ON query_snapshots(citation_state)`,
1884
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_provider_model ON query_snapshots(provider, model)`,
1885
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
1886
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON query_snapshots(created_at)`
1887
+ ]
1807
1888
  }
1808
1889
  ];
1809
1890
  function isDuplicateColumnError(err) {
@@ -1987,6 +2068,11 @@ function pickGroupRepresentative(group) {
1987
2068
  return best;
1988
2069
  }
1989
2070
 
2071
+ // ../db/src/snapshot-helpers.ts
2072
+ function filterTrackedSnapshots(rows) {
2073
+ return rows.filter((r) => r.queryId !== null);
2074
+ }
2075
+
1990
2076
  // ../intelligence/src/regressions.ts
1991
2077
  function detectRegressions(currentRun, previousRun) {
1992
2078
  const regressions = [];
@@ -3707,6 +3793,7 @@ export {
3707
3793
  migrate,
3708
3794
  groupRunsByCreatedAt,
3709
3795
  pickGroupRepresentative,
3796
+ filterTrackedSnapshots,
3710
3797
  isBlogShapedQuery,
3711
3798
  buildInventory,
3712
3799
  buildContentTargetRows,
@@ -44,6 +44,7 @@ import {
44
44
  discoverySessions,
45
45
  dropLegacyCredentialColumns,
46
46
  extractLegacyCredentials,
47
+ filterTrackedSnapshots,
47
48
  gaAiReferrals,
48
49
  gaSocialReferrals,
49
50
  gaTrafficSnapshots,
@@ -70,7 +71,7 @@ import {
70
71
  schedules,
71
72
  trafficSources,
72
73
  usageCounters
73
- } from "./chunk-BJXHETQW.js";
74
+ } from "./chunk-7256SFYT.js";
74
75
  import {
75
76
  AGENT_MEMORY_VALUE_MAX_BYTES,
76
77
  AGENT_PROVIDER_IDS,
@@ -2292,7 +2293,7 @@ async function analyticsRoutes(app) {
2292
2293
  });
2293
2294
  }
2294
2295
  const runIds = projectRuns.map((r) => r.id);
2295
- const rawSnapshots = app.db.select({
2296
+ const rawSnapshots = filterTrackedSnapshots(app.db.select({
2296
2297
  runId: querySnapshots.runId,
2297
2298
  queryId: querySnapshots.queryId,
2298
2299
  provider: querySnapshots.provider,
@@ -2300,7 +2301,7 @@ async function analyticsRoutes(app) {
2300
2301
  answerMentioned: querySnapshots.answerMentioned,
2301
2302
  answerText: querySnapshots.answerText,
2302
2303
  createdAt: querySnapshots.createdAt
2303
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all();
2304
+ }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all());
2304
2305
  const allSnapshots = rawSnapshots.map((s) => ({
2305
2306
  ...s,
2306
2307
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2339,13 +2340,13 @@ async function analyticsRoutes(app) {
2339
2340
  const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
2340
2341
  const consistencyMap = /* @__PURE__ */ new Map();
2341
2342
  if (windowRunIds.length > 0) {
2342
- const allWindowSnaps = app.db.select({
2343
+ const allWindowSnaps = filterTrackedSnapshots(app.db.select({
2343
2344
  queryId: querySnapshots.queryId,
2344
2345
  runId: querySnapshots.runId,
2345
2346
  citationState: querySnapshots.citationState,
2346
2347
  answerMentioned: querySnapshots.answerMentioned,
2347
2348
  answerText: querySnapshots.answerText
2348
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2349
+ }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all());
2349
2350
  for (const s of allWindowSnaps) {
2350
2351
  const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
2351
2352
  let entry = consistencyMap.get(s.queryId);
@@ -2358,7 +2359,7 @@ async function analyticsRoutes(app) {
2358
2359
  if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(timePoint);
2359
2360
  }
2360
2361
  }
2361
- const rawSnapshots = app.db.select({
2362
+ const rawSnapshots = filterTrackedSnapshots(app.db.select({
2362
2363
  queryId: querySnapshots.queryId,
2363
2364
  query: queries.query,
2364
2365
  provider: querySnapshots.provider,
@@ -2366,7 +2367,7 @@ async function analyticsRoutes(app) {
2366
2367
  answerMentioned: querySnapshots.answerMentioned,
2367
2368
  answerText: querySnapshots.answerText,
2368
2369
  competitorOverlap: querySnapshots.competitorOverlap
2369
- }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all();
2370
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all());
2370
2371
  const snapshots = rawSnapshots.map((s) => ({
2371
2372
  ...s,
2372
2373
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -5173,7 +5174,7 @@ function buildCandidateQueries(opts) {
5173
5174
  const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
5174
5175
  const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
5175
5176
  const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
5176
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
5177
+ const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
5177
5178
  const snapshotsByQuery = /* @__PURE__ */ new Map();
5178
5179
  for (const row of snapshotRows) {
5179
5180
  const list = snapshotsByQuery.get(row.queryId) ?? [];
@@ -5392,7 +5393,7 @@ function loadSnapshotsForRun(db, runId) {
5392
5393
  function loadSnapshotsForRunIds(db, runIds) {
5393
5394
  if (runIds.length === 0) return [];
5394
5395
  const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
5395
- return rows.map((r) => ({
5396
+ return rows.filter((r) => r.queryId !== null).map((r) => ({
5396
5397
  id: r.id,
5397
5398
  runId: r.runId,
5398
5399
  queryId: r.queryId,
@@ -6761,8 +6762,9 @@ async function citationRoutes(app) {
6761
6762
  if (rawSnapshots.length === 0) {
6762
6763
  return reply.send(emptyCitationVisibility("no-runs-yet"));
6763
6764
  }
6764
- const snapshots = rawSnapshots.map((s) => ({
6765
+ const snapshots = rawSnapshots.filter((s) => s.queryId !== null).map((s) => ({
6765
6766
  ...s,
6767
+ queryId: s.queryId,
6766
6768
  runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
6767
6769
  }));
6768
6770
  const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
@@ -6998,7 +7000,7 @@ async function compositeRoutes(app) {
6998
7000
  const limit = clampSearchLimit(request.query.limit);
6999
7001
  const escaped = escapeLikePattern(rawQuery);
7000
7002
  const pattern = `%${escaped}%`;
7001
- const snapshotMatches = app.db.select({
7003
+ const snapshotMatches = filterTrackedSnapshots(app.db.select({
7002
7004
  id: querySnapshots.id,
7003
7005
  runId: querySnapshots.runId,
7004
7006
  queryId: querySnapshots.queryId,
@@ -7020,7 +7022,7 @@ async function compositeRoutes(app) {
7020
7022
  like(queries.query, pattern)
7021
7023
  )
7022
7024
  )
7023
- ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
7025
+ ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all());
7024
7026
  const insightMatches = app.db.select().from(insights).where(
7025
7027
  and6(
7026
7028
  eq15(insights.projectId, project.id),
@@ -7094,7 +7096,7 @@ function summarizeRun(run) {
7094
7096
  function loadSnapshotsByRunIds(app, runIds) {
7095
7097
  const result = /* @__PURE__ */ new Map();
7096
7098
  if (runIds.length === 0) return result;
7097
- const rows = app.db.select({
7099
+ const rows = filterTrackedSnapshots(app.db.select({
7098
7100
  runId: querySnapshots.runId,
7099
7101
  queryId: querySnapshots.queryId,
7100
7102
  provider: querySnapshots.provider,
@@ -7102,7 +7104,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7102
7104
  citationState: querySnapshots.citationState,
7103
7105
  competitorOverlap: querySnapshots.competitorOverlap,
7104
7106
  citedDomains: querySnapshots.citedDomains
7105
- }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
7107
+ }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
7106
7108
  for (const row of rows) {
7107
7109
  const list = result.get(row.runId) ?? [];
7108
7110
  list.push({
@@ -10388,7 +10390,7 @@ var routeCatalog = [
10388
10390
  method: "post",
10389
10391
  path: "/api/v1/projects/{name}/discover/run",
10390
10392
  summary: "Start a tracked-basket discovery session",
10391
- description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.85 by default) \u2192 pick canonical representatives \u2192 probe each canonical via Gemini grounding \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns immediately with `{ runId, sessionId, status: "running" }`; the actual work runs in the background. Poll `GET /projects/{name}/discover/sessions/{id}` until `status` is `completed` or `failed`.',
10393
+ description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.85 by default) \u2192 pick canonical representatives \u2192 probe each canonical via Gemini grounding \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns immediately with `{ runId, sessionId, status: "running", consolidated }`; the actual work runs in the background. Poll `GET /projects/{name}/discover/sessions/{id}` until `status` is `completed` or `failed`. Concurrent/duplicate requests for the same (project, ICP) are consolidated onto a single in-flight session: the response carries `consolidated: true` and `200 OK` instead of `201`, and the request\'s `dedupThreshold` / `maxProbes` are ignored (the in-flight session keeps its original config).',
10392
10394
  tags: ["discovery"],
10393
10395
  parameters: [nameParameter],
10394
10396
  requestBody: {
@@ -10412,7 +10414,8 @@ var routeCatalog = [
10412
10414
  }
10413
10415
  },
10414
10416
  responses: {
10415
- 201: { description: "Discovery session enqueued; returns { runId, sessionId, status }." },
10417
+ 200: { description: "An in-flight session with the same project + ICP was reused; returns { runId, sessionId, status, consolidated: true }. The request's dedupThreshold / maxProbes are ignored." },
10418
+ 201: { description: "New discovery session enqueued; returns { runId, sessionId, status, consolidated: false }." },
10416
10419
  400: { description: "Missing or invalid ICP / parameters." },
10417
10420
  404: { description: "Project not found." }
10418
10421
  }
@@ -13510,7 +13513,7 @@ async function cdpRoutes(app, opts) {
13510
13513
  const err = notFound("Run", runId);
13511
13514
  return reply.code(err.statusCode).send(err.toJSON());
13512
13515
  }
13513
- const snapshots = app.db.select({
13516
+ const snapshots = filterTrackedSnapshots(app.db.select({
13514
13517
  id: querySnapshots.id,
13515
13518
  queryId: querySnapshots.queryId,
13516
13519
  provider: querySnapshots.provider,
@@ -13518,7 +13521,7 @@ async function cdpRoutes(app, opts) {
13518
13521
  citedDomains: querySnapshots.citedDomains,
13519
13522
  screenshotPath: querySnapshots.screenshotPath,
13520
13523
  rawResponse: querySnapshots.rawResponse
13521
- }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all();
13524
+ }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all());
13522
13525
  const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
13523
13526
  const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
13524
13527
  const byQuery = /* @__PURE__ */ new Map();
@@ -20009,7 +20012,8 @@ async function doctorRoutes(app, opts) {
20009
20012
 
20010
20013
  // ../api-routes/src/discovery/routes.ts
20011
20014
  import crypto21 from "crypto";
20012
- import { eq as eq25, desc as desc13 } from "drizzle-orm";
20015
+ import { and as and16, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20016
+ var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
20013
20017
  async function discoveryRoutes(app, opts) {
20014
20018
  app.post("/projects/:name/discover/run", async (request, reply) => {
20015
20019
  const project = resolveProject(app.db, request.params.name);
@@ -20038,9 +20042,23 @@ async function discoveryRoutes(app, opts) {
20038
20042
  });
20039
20043
  }
20040
20044
  const now = (/* @__PURE__ */ new Date()).toISOString();
20041
- const sessionId = crypto21.randomUUID();
20042
- const runId = crypto21.randomUUID();
20043
- app.db.transaction((tx) => {
20045
+ const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
20046
+ const decision = app.db.transaction((tx) => {
20047
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and16(
20048
+ eq25(discoverySessions.projectId, project.id),
20049
+ eq25(discoverySessions.icpDescription, icpDescription),
20050
+ inArray8(discoverySessions.status, [
20051
+ DiscoverySessionStatuses.queued,
20052
+ DiscoverySessionStatuses.seeding,
20053
+ DiscoverySessionStatuses.probing
20054
+ ]),
20055
+ gte4(discoverySessions.createdAt, ageFloorIso)
20056
+ )).orderBy(desc13(discoverySessions.createdAt)).get();
20057
+ if (existing && existing.runId) {
20058
+ return { reused: true, sessionId: existing.id, runId: existing.runId };
20059
+ }
20060
+ const sessionId = crypto21.randomUUID();
20061
+ const runId = crypto21.randomUUID();
20044
20062
  tx.insert(discoverySessions).values({
20045
20063
  id: sessionId,
20046
20064
  projectId: project.id,
@@ -20066,17 +20084,31 @@ async function discoveryRoutes(app, opts) {
20066
20084
  entityType: "discovery_session",
20067
20085
  entityId: sessionId
20068
20086
  });
20087
+ return { reused: false, sessionId, runId };
20069
20088
  });
20089
+ if (decision.reused) {
20090
+ return reply.status(200).send({
20091
+ runId: decision.runId,
20092
+ sessionId: decision.sessionId,
20093
+ status: "running",
20094
+ consolidated: true
20095
+ });
20096
+ }
20070
20097
  opts.onDiscoveryRunRequested({
20071
- runId,
20072
- sessionId,
20098
+ runId: decision.runId,
20099
+ sessionId: decision.sessionId,
20073
20100
  projectId: project.id,
20074
20101
  icpDescription,
20075
20102
  dedupThreshold: parsed.data.dedupThreshold,
20076
20103
  maxProbes: parsed.data.maxProbes,
20077
20104
  locations
20078
20105
  });
20079
- return reply.status(201).send({ runId, sessionId, status: "running" });
20106
+ return reply.status(201).send({
20107
+ runId: decision.runId,
20108
+ sessionId: decision.sessionId,
20109
+ status: "running",
20110
+ consolidated: false
20111
+ });
20080
20112
  });
20081
20113
  app.get(
20082
20114
  "/projects/:name/discover/sessions",
@@ -23328,7 +23360,7 @@ import crypto24 from "crypto";
23328
23360
  import fs7 from "fs";
23329
23361
  import path9 from "path";
23330
23362
  import os5 from "os";
23331
- import { and as and16, eq as eq27, inArray as inArray8, sql as sql10 } from "drizzle-orm";
23363
+ import { and as and17, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
23332
23364
 
23333
23365
  // src/run-telemetry.ts
23334
23366
  import crypto23 from "crypto";
@@ -23669,7 +23701,7 @@ var JobRunner = class {
23669
23701
  this.registry = registry;
23670
23702
  }
23671
23703
  recoverStaleRuns() {
23672
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray8(runs.status, ["running", "queued"])).all();
23704
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray9(runs.status, ["running", "queued"])).all();
23673
23705
  if (stale.length === 0) return;
23674
23706
  const now = (/* @__PURE__ */ new Date()).toISOString();
23675
23707
  for (const run of stale) {
@@ -23707,7 +23739,7 @@ var JobRunner = class {
23707
23739
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
23708
23740
  }
23709
23741
  if (existingRun.status === "queued") {
23710
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and16(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
23742
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and17(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
23711
23743
  }
23712
23744
  this.throwIfRunCancelled(runId);
23713
23745
  const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
@@ -23732,7 +23764,7 @@ var JobRunner = class {
23732
23764
  }
23733
23765
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
23734
23766
  const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23735
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray8(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23767
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and17(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23736
23768
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
23737
23769
  const competitorDomains = projectCompetitors.map((c) => c.domain);
23738
23770
  const allDomains = effectiveDomains({
@@ -23821,6 +23853,7 @@ var JobRunner = class {
23821
23853
  id: snapshotId,
23822
23854
  runId,
23823
23855
  queryId: q.id,
23856
+ queryText: q.query,
23824
23857
  provider: providerName,
23825
23858
  model: raw.model,
23826
23859
  citationState,
@@ -23844,6 +23877,7 @@ var JobRunner = class {
23844
23877
  id: crypto24.randomUUID(),
23845
23878
  runId,
23846
23879
  queryId: q.id,
23880
+ queryText: q.query,
23847
23881
  provider: providerName,
23848
23882
  model: raw.model,
23849
23883
  citationState,
@@ -24068,7 +24102,7 @@ function buildPhases(input) {
24068
24102
 
24069
24103
  // src/gsc-sync.ts
24070
24104
  import crypto25 from "crypto";
24071
- import { eq as eq28, and as and17, sql as sql11 } from "drizzle-orm";
24105
+ import { eq as eq28, and as and18, sql as sql11 } from "drizzle-orm";
24072
24106
  var log2 = createLogger("GscSync");
24073
24107
  function formatDate3(d) {
24074
24108
  return d.toISOString().split("T")[0];
@@ -24120,7 +24154,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24120
24154
  });
24121
24155
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
24122
24156
  db.delete(gscSearchData).where(
24123
- and17(
24157
+ and18(
24124
24158
  eq28(gscSearchData.projectId, projectId),
24125
24159
  sql11`${gscSearchData.date} >= ${startDate}`,
24126
24160
  sql11`${gscSearchData.date} <= ${endDate}`
@@ -24209,7 +24243,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24209
24243
  }
24210
24244
  }
24211
24245
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
24212
- db.delete(gscCoverageSnapshots).where(and17(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24246
+ db.delete(gscCoverageSnapshots).where(and18(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24213
24247
  db.insert(gscCoverageSnapshots).values({
24214
24248
  id: crypto25.randomUUID(),
24215
24249
  projectId,
@@ -24232,7 +24266,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24232
24266
 
24233
24267
  // src/gsc-inspect-sitemap.ts
24234
24268
  import crypto26 from "crypto";
24235
- import { eq as eq29, and as and18 } from "drizzle-orm";
24269
+ import { eq as eq29, and as and19 } from "drizzle-orm";
24236
24270
 
24237
24271
  // src/sitemap-parser.ts
24238
24272
  var log3 = createLogger("SitemapParser");
@@ -24448,7 +24482,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
24448
24482
  }
24449
24483
  }
24450
24484
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
24451
- db.delete(gscCoverageSnapshots).where(and18(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24485
+ db.delete(gscCoverageSnapshots).where(and19(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24452
24486
  db.insert(gscCoverageSnapshots).values({
24453
24487
  id: crypto26.randomUUID(),
24454
24488
  projectId,
@@ -24659,7 +24693,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
24659
24693
  // src/commoncrawl-sync.ts
24660
24694
  import crypto28 from "crypto";
24661
24695
  import path10 from "path";
24662
- import { and as and19, eq as eq31, sql as sql12 } from "drizzle-orm";
24696
+ import { and as and20, eq as eq31, sql as sql12 } from "drizzle-orm";
24663
24697
  var log6 = createLogger("CommonCrawlSync");
24664
24698
  var INSERT_CHUNK_SIZE = 1e4;
24665
24699
  function defaultDeps() {
@@ -24850,7 +24884,7 @@ function computeSummary(rows) {
24850
24884
  // src/backlink-extract.ts
24851
24885
  import crypto29 from "crypto";
24852
24886
  import fs8 from "fs";
24853
- import { and as and20, desc as desc15, eq as eq32 } from "drizzle-orm";
24887
+ import { and as and21, desc as desc15, eq as eq32 } from "drizzle-orm";
24854
24888
  var log7 = createLogger("BacklinkExtract");
24855
24889
  function defaultDeps2() {
24856
24890
  return {
@@ -24896,7 +24930,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
24896
24930
  const targetDomain = project.canonicalDomain;
24897
24931
  db.transaction((tx) => {
24898
24932
  tx.delete(backlinkDomains).where(
24899
- and20(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
24933
+ and21(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
24900
24934
  ).run();
24901
24935
  if (rows.length > 0) {
24902
24936
  const values = rows.map((r) => ({
@@ -25313,7 +25347,7 @@ var ProviderRegistry = class {
25313
25347
 
25314
25348
  // src/scheduler.ts
25315
25349
  import cron from "node-cron";
25316
- import { and as and21, eq as eq34 } from "drizzle-orm";
25350
+ import { and as and22, eq as eq34 } from "drizzle-orm";
25317
25351
  var log9 = createLogger("Scheduler");
25318
25352
  function taskKey(projectId, kind) {
25319
25353
  return `${projectId}::${kind}`;
@@ -25358,7 +25392,7 @@ var Scheduler = class {
25358
25392
  this.stopTask(key, existing, "Stopped");
25359
25393
  this.tasks.delete(key);
25360
25394
  }
25361
- const schedule = this.db.select().from(schedules).where(and21(eq34(schedules.projectId, projectId), eq34(schedules.kind, kind))).get();
25395
+ const schedule = this.db.select().from(schedules).where(and22(eq34(schedules.projectId, projectId), eq34(schedules.kind, kind))).get();
25362
25396
  if (schedule && schedule.enabled === 1) {
25363
25397
  this.registerCronTask(schedule);
25364
25398
  }
@@ -25482,7 +25516,7 @@ var Scheduler = class {
25482
25516
  };
25483
25517
 
25484
25518
  // src/notifier.ts
25485
- import { eq as eq35, desc as desc16, and as and22, inArray as inArray9, or as or4 } from "drizzle-orm";
25519
+ import { eq as eq35, desc as desc16, and as and23, inArray as inArray10, or as or4 } from "drizzle-orm";
25486
25520
  import crypto31 from "crypto";
25487
25521
  var log10 = createLogger("Notifier");
25488
25522
  var Notifier = class {
@@ -25589,7 +25623,7 @@ var Notifier = class {
25589
25623
  computeTransitions(runId, projectId) {
25590
25624
  const thisRun = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
25591
25625
  if (!thisRun) return [];
25592
- const groupSiblings = this.db.select().from(runs).where(and22(
25626
+ const groupSiblings = this.db.select().from(runs).where(and23(
25593
25627
  eq35(runs.projectId, projectId),
25594
25628
  eq35(runs.kind, thisRun.kind),
25595
25629
  eq35(runs.createdAt, thisRun.createdAt)
@@ -25615,7 +25649,7 @@ var Notifier = class {
25615
25649
  );
25616
25650
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
25617
25651
  const recentRuns = this.db.select().from(runs).where(
25618
- and22(
25652
+ and23(
25619
25653
  eq35(runs.projectId, projectId),
25620
25654
  eq35(runs.kind, thisRun.kind),
25621
25655
  or4(eq35(runs.status, "completed"), eq35(runs.status, "partial"))
@@ -25635,19 +25669,21 @@ var Notifier = class {
25635
25669
  provider: querySnapshots.provider,
25636
25670
  location: querySnapshots.location,
25637
25671
  citationState: querySnapshots.citationState
25638
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
25672
+ }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray10(querySnapshots.runId, currentRunIds)).all();
25639
25673
  const previousSnapshots = this.db.select({
25640
25674
  queryId: querySnapshots.queryId,
25641
25675
  provider: querySnapshots.provider,
25642
25676
  location: querySnapshots.location,
25643
25677
  citationState: querySnapshots.citationState
25644
- }).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
25678
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
25645
25679
  const prevMap = /* @__PURE__ */ new Map();
25646
25680
  for (const s of previousSnapshots) {
25681
+ if (s.queryId == null) continue;
25647
25682
  prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
25648
25683
  }
25649
25684
  const transitions = [];
25650
25685
  for (const s of currentSnapshots) {
25686
+ if (s.queryId == null) continue;
25651
25687
  const key = `${s.queryId}:${s.provider}:${s.location ?? ""}`;
25652
25688
  const prevState = prevMap.get(key);
25653
25689
  if (prevState && prevState !== s.citationState) {
@@ -26152,7 +26188,7 @@ function resolveSessionProviderAndModel(config, opts) {
26152
26188
 
26153
26189
  // src/agent/memory-store.ts
26154
26190
  import crypto32 from "crypto";
26155
- import { and as and23, desc as desc17, eq as eq37, like as like2, sql as sql13 } from "drizzle-orm";
26191
+ import { and as and24, desc as desc17, eq as eq37, like as like2, sql as sql13 } from "drizzle-orm";
26156
26192
  var COMPACTION_KEY_PREFIX = "compaction:";
26157
26193
  var COMPACTION_NOTES_PER_SESSION = 3;
26158
26194
  function rowToDto2(row) {
@@ -26197,12 +26233,12 @@ function upsertMemoryEntry(db, args) {
26197
26233
  updatedAt: now
26198
26234
  }
26199
26235
  }).run();
26200
- const row = db.select().from(agentMemory).where(and23(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, args.key))).get();
26236
+ const row = db.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, args.key))).get();
26201
26237
  if (!row) throw new Error("memory upsert produced no row");
26202
26238
  return rowToDto2(row);
26203
26239
  }
26204
26240
  function deleteMemoryEntry(db, projectId, key) {
26205
- const result = db.delete(agentMemory).where(and23(eq37(agentMemory.projectId, projectId), eq37(agentMemory.key, key))).run();
26241
+ const result = db.delete(agentMemory).where(and24(eq37(agentMemory.projectId, projectId), eq37(agentMemory.key, key))).run();
26206
26242
  const changes = result.changes ?? 0;
26207
26243
  return changes > 0;
26208
26244
  }
@@ -26231,7 +26267,7 @@ function writeCompactionNote(db, args) {
26231
26267
  }).run();
26232
26268
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
26233
26269
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
26234
- and23(
26270
+ and24(
26235
26271
  eq37(agentMemory.projectId, args.projectId),
26236
26272
  like2(agentMemory.key, `${sessionPrefix}%`)
26237
26273
  )
@@ -26240,7 +26276,7 @@ function writeCompactionNote(db, args) {
26240
26276
  if (stale.length > 0) {
26241
26277
  tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
26242
26278
  }
26243
- const row = tx.select().from(agentMemory).where(and23(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, key))).get();
26279
+ const row = tx.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, key))).get();
26244
26280
  if (row) inserted = rowToDto2(row);
26245
26281
  });
26246
26282
  if (!inserted) throw new Error("compaction note write produced no row");
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  setTelemetrySource,
21
21
  showFirstRunNotice,
22
22
  trackEvent
23
- } from "./chunk-JJHBPITI.js";
23
+ } from "./chunk-7AF6B3L6.js";
24
24
  import {
25
25
  CliError,
26
26
  EXIT_SYSTEM_ERROR,
@@ -49,7 +49,7 @@ import {
49
49
  queries,
50
50
  querySnapshots,
51
51
  runs
52
- } from "./chunk-BJXHETQW.js";
52
+ } from "./chunk-7256SFYT.js";
53
53
  import {
54
54
  CcReleaseSyncStatuses,
55
55
  CheckScopes,
@@ -623,7 +623,7 @@ function readStoredGroundingSources(rawResponse) {
623
623
  return result;
624
624
  }
625
625
  async function backfillInsightsCommand(project, opts) {
626
- const { IntelligenceService } = await import("./intelligence-service-XKOUBRCE.js");
626
+ const { IntelligenceService } = await import("./intelligence-service-3P2DMYRR.js");
627
627
  const config = loadConfig();
628
628
  const db = createClient(config.database);
629
629
  migrate(db);
@@ -2020,10 +2020,17 @@ Sessions already started (recover with \`canonry discover show ${project} <id>\`
2020
2020
  }
2021
2021
  for (const { angle, start } of runs2) {
2022
2022
  if (angle) console.log(`[${angle}]`);
2023
- console.log(`Discovery run started: ${start.runId}`);
2024
- console.log(` Session: ${start.sessionId}`);
2025
- console.log(` Status: ${start.status}`);
2026
- console.log(` Tail: canonry discover show ${project} ${start.sessionId}`);
2023
+ if (start.consolidated) {
2024
+ console.log(`Reusing in-flight discovery session: ${start.sessionId}`);
2025
+ console.log(` Run: ${start.runId}`);
2026
+ console.log(` Status: ${start.status}`);
2027
+ console.log(` Tail: canonry discover show ${project} ${start.sessionId}`);
2028
+ } else {
2029
+ console.log(`Discovery run started: ${start.runId}`);
2030
+ console.log(` Session: ${start.sessionId}`);
2031
+ console.log(` Status: ${start.status}`);
2032
+ console.log(` Tail: canonry discover show ${project} ${start.sessionId}`);
2033
+ }
2027
2034
  if (runs2.length > 1) console.log();
2028
2035
  }
2029
2036
  return;
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-JJHBPITI.js";
3
+ } from "./chunk-7AF6B3L6.js";
4
4
  import {
5
5
  loadConfig
6
6
  } from "./chunk-5EBN7736.js";
7
- import "./chunk-BJXHETQW.js";
7
+ import "./chunk-7256SFYT.js";
8
8
  import "./chunk-XW3F5EEW.js";
9
9
  export {
10
10
  createServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-BJXHETQW.js";
3
+ } from "./chunk-7256SFYT.js";
4
4
  import "./chunk-XW3F5EEW.js";
5
5
  export {
6
6
  IntelligenceService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.33.0",
3
+ "version": "4.34.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -60,23 +60,23 @@
60
60
  "@types/node-cron": "^3.0.11",
61
61
  "tsup": "^8.5.1",
62
62
  "tsx": "^4.19.0",
63
+ "@ainyc/canonry-config": "0.0.0",
63
64
  "@ainyc/canonry-api-routes": "0.0.0",
64
65
  "@ainyc/canonry-contracts": "0.0.0",
65
- "@ainyc/canonry-intelligence": "0.0.0",
66
66
  "@ainyc/canonry-db": "0.0.0",
67
- "@ainyc/canonry-integration-cloud-run": "0.0.0",
68
- "@ainyc/canonry-config": "0.0.0",
69
67
  "@ainyc/canonry-integration-bing": "0.0.0",
70
- "@ainyc/canonry-integration-google": "0.0.0",
68
+ "@ainyc/canonry-integration-cloud-run": "0.0.0",
71
69
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
72
- "@ainyc/canonry-integration-wordpress": "0.0.0",
73
- "@ainyc/canonry-provider-cdp": "0.0.0",
74
- "@ainyc/canonry-provider-gemini": "0.0.0",
70
+ "@ainyc/canonry-intelligence": "0.0.0",
71
+ "@ainyc/canonry-integration-google": "0.0.0",
75
72
  "@ainyc/canonry-integration-traffic": "0.0.0",
73
+ "@ainyc/canonry-provider-cdp": "0.0.0",
74
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
76
75
  "@ainyc/canonry-provider-claude": "0.0.0",
77
- "@ainyc/canonry-provider-perplexity": "0.0.0",
76
+ "@ainyc/canonry-provider-gemini": "0.0.0",
77
+ "@ainyc/canonry-provider-local": "0.0.0",
78
78
  "@ainyc/canonry-provider-openai": "0.0.0",
79
- "@ainyc/canonry-provider-local": "0.0.0"
79
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",