@canonry/canonry 7.7.0 → 7.8.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.
Files changed (34) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +43 -17
  2. package/assets/agent-workspace/skills/aero/references/agent-operations.md +4 -1
  3. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +15 -7
  4. package/assets/agent-workspace/skills/canonry/SKILL.md +4 -1
  5. package/assets/assets/{AuditHistoryPanel-qumC-I56.js → AuditHistoryPanel-AJlkzSmq.js} +1 -1
  6. package/assets/assets/{BacklinksPage-BNyYcY5w.js → BacklinksPage-IhPtSh39.js} +1 -1
  7. package/assets/assets/{HistoryPage-CQfOM6NA.js → HistoryPage-B-vNinvd.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-D0LTQHL7.js → MeasurementPropertyPage-s1kKo2TD.js} +1 -1
  9. package/assets/assets/{ProjectPage-QQErJAT8.js → ProjectPage-iH_9g_lU.js} +1 -1
  10. package/assets/assets/{RunsPage-CniAGDVF.js → RunsPage-BSbEUlNZ.js} +1 -1
  11. package/assets/assets/{SettingsPage-BPsrE4ao.js → SettingsPage-zHyljW2M.js} +1 -1
  12. package/assets/assets/SiteHealthSection-AYGPjoka.js +4 -0
  13. package/assets/assets/{TrafficPage-DXKk92zy.js → TrafficPage-CY_qggoW.js} +1 -1
  14. package/assets/assets/{TrafficSourceDetailPage-4POLIA_c.js → TrafficSourceDetailPage-aJ2Vk6Pd.js} +1 -1
  15. package/assets/assets/{extract-error-message-C6t0hB2w.js → extract-error-message-CHEe8sOP.js} +1 -1
  16. package/assets/assets/index-DfamSNSD.js +86 -0
  17. package/assets/assets/{react-sigma_core.esm.min-D7GjzWYV.js → react-sigma_core.esm.min-2sZRqoN4.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-ChE5k_dV.js → v2-overview-adapter-ZNgNU_Yp.js} +1 -1
  19. package/assets/index.html +1 -1
  20. package/dist/{chunk-ODEAE5T5.js → chunk-AAMKTL6B.js} +3 -3
  21. package/dist/{chunk-WJJTKZKK.js → chunk-DO7XPDG5.js} +5977 -5743
  22. package/dist/{chunk-75RSUOHZ.js → chunk-KRFTA4I7.js} +1126 -475
  23. package/dist/{chunk-LFUSODDL.js → chunk-L35TGPAV.js} +444 -158
  24. package/dist/{chunk-WBTYXLXL.js → chunk-LO56QXPO.js} +5 -4
  25. package/dist/{chunk-JJXTHJ4E.js → chunk-T2V2PESH.js} +4 -3
  26. package/dist/{chunk-3DB43KT2.js → chunk-ZLB7WX5D.js} +26 -33
  27. package/dist/cli.js +67 -28
  28. package/dist/{demo-server-UWBPY743.js → demo-server-KDMG3APY.js} +4 -4
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-LOH37GPJ.js → intelligence-service-LAXJFE3U.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +7 -7
  33. package/assets/assets/SiteHealthSection-zW3ykBCu.js +0 -4
  34. package/assets/assets/index-DBQ-8jB1.js +0 -86
@@ -20,20 +20,20 @@ import {
20
20
  saveConfig,
21
21
  saveConfigPatch,
22
22
  sentimentInstallReadiness
23
- } from "./chunk-WBTYXLXL.js";
23
+ } from "./chunk-LO56QXPO.js";
24
24
  import {
25
25
  computeFactorAverages,
26
26
  deleteSiteCrawlGraphLayout,
27
27
  persistSiteCrawlGraphLayout,
28
28
  prepareSiteCrawlGraphLayout,
29
29
  recordedSharePct
30
- } from "./chunk-JJXTHJ4E.js";
30
+ } from "./chunk-T2V2PESH.js";
31
31
  import {
32
32
  CanonryMcpToolNames,
33
33
  CliError,
34
34
  PACKAGE_VERSION,
35
35
  canonryMcpTools
36
- } from "./chunk-3DB43KT2.js";
36
+ } from "./chunk-ZLB7WX5D.js";
37
37
  import {
38
38
  CC_CACHE_DIR,
39
39
  DUCKDB_SPEC,
@@ -205,7 +205,7 @@ import {
205
205
  siteLivenessState,
206
206
  toAlertView,
207
207
  usageCounters
208
- } from "./chunk-75RSUOHZ.js";
208
+ } from "./chunk-KRFTA4I7.js";
209
209
  import {
210
210
  AGENT_MEMORY_VALUE_MAX_BYTES,
211
211
  AGENT_NONE,
@@ -238,6 +238,7 @@ import {
238
238
  LlmCapabilities,
239
239
  MEASUREMENT_CHANGES_NOISE_ANSWERS,
240
240
  MEASUREMENT_PLAN_V2_SCHEMA_VERSION,
241
+ MeasurementPortfolioLists,
241
242
  MemorySources,
242
243
  ONBOARDING_FLOW_VERSION,
243
244
  OUTSTANDING_PROVIDER_BATCH_STATUSES,
@@ -403,6 +404,7 @@ import {
403
404
  templateLinkUbiquityAvailable,
404
405
  textContainsBrandAlias,
405
406
  textContainsDomain,
407
+ truncateUtf16,
406
408
  updateCheckEnvOptOut,
407
409
  upgradeCaveatFor,
408
410
  upgradeCommandFor,
@@ -412,7 +414,7 @@ import {
412
414
  visibilityReportRequestSchema,
413
415
  winnabilityClassLabel,
414
416
  withRetry
415
- } from "./chunk-WJJTKZKK.js";
417
+ } from "./chunk-DO7XPDG5.js";
416
418
 
417
419
  // src/runtime-context.ts
418
420
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -7870,7 +7872,7 @@ function nonBlank(value) {
7870
7872
  return trimmed ? trimmed : void 0;
7871
7873
  }
7872
7874
  function resolveBuildCommit(env = process.env) {
7873
- const embedded = true ? "0860f29cce58a29483357e3fd66869877e7a393c" : void 0;
7875
+ const embedded = true ? "6f387d86ae72c2505e9c3d5f17e1bc7f45397872" : void 0;
7874
7876
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
7875
7877
  }
7876
7878
  function resolveInstanceIdentity(env = process.env) {
@@ -16262,7 +16264,7 @@ import { Type as Type3 } from "@earendil-works/pi-ai";
16262
16264
 
16263
16265
  // src/agent/mcp-to-agent-tool.ts
16264
16266
  import { Type as Type2 } from "@earendil-works/pi-ai";
16265
- import { randomUUID as randomUUID2 } from "crypto";
16267
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
16266
16268
 
16267
16269
  // src/agent/tool-result-units.ts
16268
16270
  var SUCCESS_STATUSES = ["200", "201", "202"];
@@ -16353,16 +16355,25 @@ function renderRatioUnits(schema, value, components = {}) {
16353
16355
 
16354
16356
  // src/agent/mcp-to-agent-tool.ts
16355
16357
  var MAX_TOOL_RESULT_CHARS = 2e4;
16356
- var TRUNCATION_NOTE = "... (truncated, result too large)";
16357
16358
  var TRUNCATION_SUMMARY_KEY = "__truncation";
16358
16359
  var PARTIAL_LISTS_KEY = "__partialLists";
16359
16360
  var CURSOR_KEYS = ["nextCursor", "nextOffset", "nextPageToken", "next_cursor"];
16361
+ var PORTFOLIO_PAGE_LISTS = /* @__PURE__ */ new Map([
16362
+ [MeasurementPortfolioLists["weakest-properties"], { path: "weakestProperties", totalKey: "totalProperties" }],
16363
+ [MeasurementPortfolioLists["strongest-mentions"], { path: "mentionRanking.strongest", totalKey: "eligiblePropertyCount" }],
16364
+ [MeasurementPortfolioLists["weakest-mentions"], { path: "mentionRanking.weakest", totalKey: "eligiblePropertyCount" }],
16365
+ [MeasurementPortfolioLists["excluded-mentions"], { path: "mentionRanking.excluded", totalKey: "excludedTotal" }],
16366
+ [MeasurementPortfolioLists.markets, { path: "markets", totalKey: "totalMarkets" }],
16367
+ [MeasurementPortfolioLists["observed-names"], { path: "answerEvidence.observedNames", totalKey: "observedNamesTotal" }],
16368
+ [MeasurementPortfolioLists["cited-domains"], { path: "answerEvidence.citedDomains", totalKey: "citedDomainsTotal" }]
16369
+ ]);
16360
16370
  var MAX_STRUCTURED_TRUNCATION_CHARS = 2e6;
16361
16371
  var TRUNCATION_SERIALIZE_BUDGET_CHARS = 64e6;
16362
16372
  var SMALL_LIST_ROWS = 25;
16363
- var MAX_SUMMARY_DEPTH = 5;
16364
16373
  var MAX_SUMMARY_KEYS = 30;
16365
- var MAX_SUMMARY_SEGMENT_CHARS = 60;
16374
+ var MAX_SUMMARY_PATH_CHARS = 96;
16375
+ var PROJECTION_SUMMARY_CHARS = 5e3;
16376
+ var PROJECTION_METADATA_CHARS = PROJECTION_SUMMARY_CHARS - 500;
16366
16377
  var MAX_PARTIAL_DEPTH = 4;
16367
16378
  var MAX_PARTIAL_NOTES = 12;
16368
16379
  function serializeResult(value) {
@@ -16381,10 +16392,47 @@ function keptSummary(counts, cursors = {}) {
16381
16392
  if (Object.keys(cursors).length > 0) summary.cursors = cursors;
16382
16393
  return summary;
16383
16394
  }
16384
- function noteCursor(owner, ownerPath, listPath, kept, total, notes) {
16385
- const key = CURSOR_KEYS.find((candidate) => owner[candidate] !== void 0 && owner[candidate] !== null);
16386
- if (!key || kept >= total) return;
16387
- notes[ownerPath ? `${ownerPath}.${key}` : key] = `skips the ${total - kept} rows cut from ${listPath}; to read them, call again with ${kept > 0 ? `limit <= ${kept}` : "a lower limit"}`;
16395
+ function noteCursor(owner, ownerPath, listPath, kept, total, notes, root = owner) {
16396
+ const cursor = cursorForList(owner, ownerPath, listPath.slice(listPath.lastIndexOf(".") + 1), root);
16397
+ if (!cursor || kept >= total) return;
16398
+ notes[cursor.path] = `incomplete page: skips the ${total - kept} rows cut from ${listPath}; re-request the original cursor with ${kept > 0 ? `${cursor.limit} <= ${kept}` : `a lower ${cursor.limit}`}`;
16399
+ }
16400
+ function cursorForList(owner, ownerPath, listKey, root = owner) {
16401
+ const prefix = ownerPath ? `${ownerPath}.` : "";
16402
+ if (listKey === "queries" && isRecord5(owner.queryPage) && owner.queryPage.nextCursor !== void 0 && owner.queryPage.nextCursor !== null) {
16403
+ const page = owner.queryPage;
16404
+ return {
16405
+ path: `${prefix}queryPage.nextCursor`,
16406
+ value: page.nextCursor,
16407
+ parameter: "queryCursor",
16408
+ limit: "queryLimit",
16409
+ ...typeof page.total === "number" ? { total: { count: page.total, text: String(page.total) } } : {}
16410
+ };
16411
+ }
16412
+ if (listKey === "attempts" && owner.nextAttemptCursor !== void 0 && owner.nextAttemptCursor !== null) {
16413
+ return {
16414
+ path: `${prefix}nextAttemptCursor`,
16415
+ value: owner.nextAttemptCursor,
16416
+ parameter: "attemptCursor",
16417
+ limit: "attemptLimit",
16418
+ ...typeof owner.attemptCount === "number" ? { total: { count: owner.attemptCount, text: String(owner.attemptCount) } } : {}
16419
+ };
16420
+ }
16421
+ const selected = typeof root.pageList === "string" ? PORTFOLIO_PAGE_LISTS.get(root.pageList) : void 0;
16422
+ const selectedList = selected?.path === `${prefix}${listKey}`;
16423
+ if (selected && owner === root && !selectedList) return void 0;
16424
+ const cursorOwner = selectedList ? root : owner;
16425
+ const cursorPrefix = selectedList ? "" : prefix;
16426
+ const key = CURSOR_KEYS.find((candidate) => cursorOwner[candidate] !== void 0 && cursorOwner[candidate] !== null);
16427
+ if (!key) return void 0;
16428
+ const total = selectedList ? owner[selected.totalKey] : void 0;
16429
+ return {
16430
+ path: `${cursorPrefix}${key}`,
16431
+ value: cursorOwner[key],
16432
+ parameter: "cursor",
16433
+ limit: "limit",
16434
+ ...typeof total === "number" ? { total: { count: total, text: String(total) } } : {}
16435
+ };
16388
16436
  }
16389
16437
  function ownListTotal(numbers, key) {
16390
16438
  const lower = key.toLowerCase();
@@ -16412,9 +16460,14 @@ function partialLists(root) {
16412
16460
  }
16413
16461
  let noted = false;
16414
16462
  if (lists.length <= MAX_SUMMARY_KEYS) {
16415
- const counts = lists.map(([key]) => ownListTotal(numbers, key));
16463
+ const counts = lists.map(([key]) => ownListTotal(numbers, key) ?? cursorForList(object, path9, key, root)?.total);
16416
16464
  const uncounted = counts.flatMap((count3, index) => count3 ? [] : [index]);
16417
- if (uncounted.length === 1) counts[uncounted[0]] = bareTotal(numbers);
16465
+ if (uncounted.length === 1) {
16466
+ const index = uncounted[0];
16467
+ const key = lists[index][0];
16468
+ const paged = cursorForList(object, path9, key, root) || typeof object.truncated === "boolean" || typeof object[`${key}Truncated`] === "boolean" || ["items", "rows", "results"].includes(key);
16469
+ if (paged) counts[index] = bareTotal(numbers);
16470
+ }
16418
16471
  for (const [index, [key, rows]] of lists.entries()) {
16419
16472
  const label = path9 ? `${path9}.${key}` : key;
16420
16473
  const count3 = counts[index];
@@ -16442,9 +16495,25 @@ function withPartialLists(value) {
16442
16495
  if (!isRecord5(value) || typeof value.toJSON === "function") return value;
16443
16496
  const hadNote = Object.hasOwn(value, PARTIAL_LISTS_KEY);
16444
16497
  const notes = partialLists(value);
16445
- if (!notes && !hadNote) return value;
16446
- const { [PARTIAL_LISTS_KEY]: _previous, ...rest } = value;
16447
- return notes ? { [PARTIAL_LISTS_KEY]: notes, ...rest } : rest;
16498
+ if (!notes && !hadNote && value.nextAttemptCursor == null && !CURSOR_KEYS.some((key) => value[key] !== void 0 && value[key] !== null) && !Object.values(value).some((child) => isRecord5(child) && CURSOR_KEYS.some((key) => child[key] !== void 0 && child[key] !== null))) return value;
16499
+ const pagination = {};
16500
+ const visit = (owner, path9, depth) => {
16501
+ const lists = Object.entries(owner).filter((entry) => Array.isArray(entry[1]));
16502
+ const numbers = Object.entries(owner).filter((entry) => typeof entry[1] === "number");
16503
+ for (const [key, rows] of lists) {
16504
+ const cursor = cursorForList(owner, path9, key, value);
16505
+ if (!cursor || key.startsWith("__") || Object.keys(pagination).length >= MAX_PARTIAL_NOTES) continue;
16506
+ const label = path9 ? `${path9}.${key}` : key;
16507
+ const total = cursor.total ?? ownListTotal(numbers, key) ?? (lists.length === 1 ? bareTotal(numbers) : void 0);
16508
+ const incomplete = owner.__truncated === true || isRecord5(owner.__omittedRowsByField) && typeof owner.__omittedRowsByField[key] === "number";
16509
+ pagination[label] = `showing ${rows.length}${total ? ` of ${total.text}` : " rows"}; ${incomplete ? `incomplete page: re-request the original cursor with a smaller ${cursor.limit}; the next cursor skips omitted rows` : `pass ${cursor.parameter} ${JSON.stringify(cursor.value)}`}`;
16510
+ }
16511
+ if (depth >= MAX_PARTIAL_DEPTH) return;
16512
+ for (const [key, child] of Object.entries(owner)) if (!key.startsWith("__") && isRecord5(child)) visit(child, path9 ? `${path9}.${key}` : key, depth + 1);
16513
+ };
16514
+ visit(value, "", 1);
16515
+ const { [PARTIAL_LISTS_KEY]: _previous, __pagination: _pagination, ...rest } = value;
16516
+ return { ...notes ? { [PARTIAL_LISTS_KEY]: notes } : {}, ...Object.keys(pagination).length > 0 ? { __pagination: pagination } : {}, ...rest };
16448
16517
  }
16449
16518
  function largestArrayKey(obj) {
16450
16519
  let best;
@@ -16569,7 +16638,7 @@ function currentSummary(root, owned) {
16569
16638
  count3.kept += value.length;
16570
16639
  count3.total += list.rows.length;
16571
16640
  counts.set(list.path, count3);
16572
- noteCursor(object, path9, list.path, value.length, list.rows.length, cursors);
16641
+ noteCursor(object, path9, list.path, value.length, list.rows.length, cursors, root);
16573
16642
  }
16574
16643
  for (const row of value) if (isRecord5(row)) visit(row, `${valuePath}[]`);
16575
16644
  } else if (isRecord5(value)) {
@@ -16672,123 +16741,107 @@ function hasOtherLists(value, primary) {
16672
16741
  }
16673
16742
  return isRecord5(value) && Object.values(value).some((child) => hasOtherLists(child, primary));
16674
16743
  }
16675
- function shortSegment(key) {
16676
- return key.length > MAX_SUMMARY_SEGMENT_CHARS ? `${key.slice(0, MAX_SUMMARY_SEGMENT_CHARS)}...` : key;
16677
- }
16678
- function childPath(container) {
16679
- if (container.kind === "array") return `${container.path}[${container.complete}]`;
16680
- const key = shortSegment(container.key ?? "");
16681
- return container.path ? `${container.path}.${key}` : key;
16682
- }
16683
- function serializedKeys(value) {
16684
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
16685
- const record2 = value;
16686
- if (typeof record2.toJSON === "function") return void 0;
16687
- return Object.keys(record2).filter((key) => {
16688
- const child = record2[key];
16689
- return child !== void 0 && typeof child !== "function" && typeof child !== "symbol";
16690
- });
16744
+ function projectionPath(path9) {
16745
+ if (path9.length <= MAX_SUMMARY_PATH_CHARS) return path9;
16746
+ const digest = createHash5("sha256").update(path9).digest("hex").slice(0, 12);
16747
+ return `${truncateUtf16(path9, 48)}...#${digest}${path9.slice(-24)}`;
16691
16748
  }
16692
- function openContainersAt(shown, details) {
16693
- const stack = [];
16694
- let inString = false;
16695
- let escaped = false;
16696
- let stringStart = 0;
16697
- for (let i = 0; i < shown.length; i++) {
16698
- const ch = shown[i];
16699
- const top = stack.at(-1);
16700
- if (inString) {
16701
- if (escaped) escaped = false;
16702
- else if (ch === "\\") escaped = true;
16703
- else if (ch === '"') {
16704
- inString = false;
16705
- if (top?.kind === "object" && top.expectKey) {
16706
- top.key = JSON.parse(shown.slice(stringStart, i + 1));
16707
- top.expectKey = false;
16749
+ function boundedProjection(full) {
16750
+ const value = JSON.parse(full);
16751
+ const summary = {
16752
+ droppedKeys: [],
16753
+ keptItems: {},
16754
+ projection: "Only complete array rows and marked partial string prefixes are shown; omitted fields are not evidence. Never follow a cursor from a cut page; re-request the original cursor with a smaller limit."
16755
+ };
16756
+ const skippedMetadata = () => {
16757
+ summary.moreMetadataEntries = (summary.moreMetadataEntries ?? 0) + 1;
16758
+ };
16759
+ const record2 = (entries, path9, value2) => {
16760
+ if (Object.keys(entries).length >= MAX_SUMMARY_KEYS) {
16761
+ skippedMetadata();
16762
+ return false;
16763
+ }
16764
+ const key = projectionPath(path9);
16765
+ entries[key] = value2;
16766
+ if (serializeResult(summary).length <= PROJECTION_METADATA_CHARS) return true;
16767
+ delete entries[key];
16768
+ skippedMetadata();
16769
+ return false;
16770
+ };
16771
+ let dropped = 0;
16772
+ const omit = (path9) => {
16773
+ dropped++;
16774
+ if (summary.droppedKeys.length < MAX_SUMMARY_KEYS) {
16775
+ summary.droppedKeys.push(projectionPath(path9));
16776
+ if (serializeResult(summary).length > PROJECTION_METADATA_CHARS) summary.droppedKeys.pop();
16777
+ }
16778
+ if (dropped > summary.droppedKeys.length) summary.moreDroppedKeys = dropped - summary.droppedKeys.length;
16779
+ };
16780
+ const project = (input, budget, path9, owner, ownerPath = "") => {
16781
+ if (Array.isArray(input)) {
16782
+ const rows = [];
16783
+ let used2 = 2;
16784
+ for (const row of input) {
16785
+ const text = serializeResult(row);
16786
+ const cost = text.length + (rows.length > 0 ? 1 : 0);
16787
+ if (used2 + cost > budget) break;
16788
+ rows.push(text);
16789
+ used2 += cost;
16790
+ }
16791
+ if (rows.length < input.length) {
16792
+ record2(summary.keptItems, path9 || "items", `${rows.length} of ${input.length}`);
16793
+ if (rows.length === 0) omit(path9 || "items");
16794
+ if (owner) {
16795
+ const cursors = {};
16796
+ noteCursor(owner, ownerPath, path9, rows.length, input.length, cursors, isRecord5(value) ? value : owner);
16797
+ for (const [cursor, note] of Object.entries(cursors)) {
16798
+ record2(summary.cursors ??= {}, cursor, note.replace(path9, projectionPath(path9)));
16799
+ }
16708
16800
  }
16709
16801
  }
16710
- continue;
16802
+ return `[${rows.join(",")}]`;
16711
16803
  }
16712
- if (ch === '"') {
16713
- inString = true;
16714
- stringStart = i;
16715
- } else if (ch === "{" || ch === "[") {
16716
- let value = details;
16717
- if (top) {
16718
- const parent = top.value;
16719
- value = top.kind === "array" ? Array.isArray(parent) ? parent[top.complete] : void 0 : parent && typeof parent === "object" ? parent[top.key ?? ""] : void 0;
16720
- }
16721
- stack.push({
16722
- kind: ch === "{" ? "object" : "array",
16723
- path: top ? childPath(top) : "",
16724
- value,
16725
- complete: 0,
16726
- childClosed: false,
16727
- expectKey: ch === "{"
16728
- });
16729
- } else if (ch === "}" || ch === "]") {
16730
- stack.pop();
16731
- const parent = stack.at(-1);
16732
- if (parent) parent.childClosed = true;
16733
- } else if (ch === "," && top) {
16734
- top.complete++;
16735
- top.childClosed = false;
16736
- top.expectKey = top.kind === "object";
16737
- }
16738
- }
16739
- return stack;
16740
- }
16741
- function sliceSummaryLine(shown, details) {
16742
- if (!shown.startsWith("{") && !shown.startsWith("[")) return "";
16743
- const stack = openContainersAt(shown, details);
16744
- const root = stack.at(0);
16745
- const deepest = stack.at(-1);
16746
- if (!root || !deepest) return "";
16747
- const summary = {
16748
- // Between two children the cut is named by their container.
16749
- cutAt: deepest.childClosed || deepest.expectKey ? deepest.path : childPath(deepest),
16750
- droppedKeys: [],
16751
- keptItems: {}
16804
+ if (!isRecord5(input)) {
16805
+ const text = serializeResult(input);
16806
+ if (text.length <= budget) return text;
16807
+ if (typeof input === "string" && budget > 2) {
16808
+ const kept = largestFitting(Math.min(input.length, budget - 2), (length) => serializeResult(truncateUtf16(input, length)).length <= budget);
16809
+ const prefix = truncateUtf16(input, kept);
16810
+ if (prefix.length > 0 && record2(summary.slicedKeys ??= {}, path9 || "(root)", { keptChars: prefix.length, totalChars: input.length })) return serializeResult(prefix);
16811
+ }
16812
+ omit(path9 || "(root)");
16813
+ return void 0;
16814
+ }
16815
+ const fields = Object.entries(input).filter(([key]) => !key.startsWith("__"));
16816
+ const priority = ([key, child]) => key === "queryPage" && isRecord5(child) ? 0 : typeof child === "string" && child.length > budget ? 2 : Number(typeof child === "object" && child !== null);
16817
+ fields.sort((a, b) => priority(a) - priority(b));
16818
+ const shown2 = [];
16819
+ let used = 2;
16820
+ for (const [key, child] of fields) {
16821
+ const childPath = path9 ? `${path9}.${key}` : key;
16822
+ const prefix = `${JSON.stringify(key)}:`;
16823
+ const allowance = budget - used - prefix.length - (shown2.length > 0 ? 1 : 0);
16824
+ if (allowance < 2) {
16825
+ omit(childPath);
16826
+ continue;
16827
+ }
16828
+ const text = project(child, allowance, childPath, input, path9);
16829
+ if (text === void 0) continue;
16830
+ shown2.push(prefix + text);
16831
+ used += prefix.length + text.length + (shown2.length > 1 ? 1 : 0);
16832
+ }
16833
+ return `{${shown2.join(",")}}`;
16752
16834
  };
16753
- for (const container of stack.slice(0, MAX_SUMMARY_DEPTH)) {
16754
- const total = container.kind === "array" ? Array.isArray(container.value) ? container.value.length : void 0 : serializedKeys(container.value)?.length;
16755
- if (total === void 0) break;
16756
- if (container === root && container.kind === "object") continue;
16757
- const shownCount = container.complete + (container.childClosed ? 1 : 0);
16758
- summary.keptItems[container.path || "(root)"] = container.kind === "array" ? `${shownCount} of ${total}` : `${shownCount} of ${total} keys`;
16759
- }
16760
- const rootKeys = root.kind === "object" ? serializedKeys(root.value) : void 0;
16761
- if (rootKeys) {
16762
- const inProgress = root.expectKey || root.childClosed ? 0 : 1;
16763
- const dropped = rootKeys.slice(root.complete + (root.childClosed ? 1 : 0) + inProgress);
16764
- const record2 = root.value;
16765
- for (const key of dropped.slice(0, MAX_SUMMARY_KEYS)) {
16766
- const label = shortSegment(key);
16767
- summary.droppedKeys.push(label);
16768
- const value = record2[key];
16769
- if (Array.isArray(value)) summary.keptItems[label] = `0 of ${value.length}`;
16770
- }
16771
- if (dropped.length > MAX_SUMMARY_KEYS) summary.moreDroppedKeys = dropped.length - MAX_SUMMARY_KEYS;
16772
- }
16773
- return `${TRUNCATION_SUMMARY_KEY}: ${JSON.stringify(summary)}`;
16774
- }
16775
- function markedSlice(full, details) {
16776
- let cut = MAX_TOOL_RESULT_CHARS;
16777
- for (let attempt = 0; attempt < 4; attempt++) {
16778
- const line = sliceSummaryLine(full.slice(0, cut), details);
16779
- const cost = line ? line.length + 1 : 0;
16780
- if (cut + cost <= MAX_TOOL_RESULT_CHARS) {
16781
- return full.slice(0, cut) + "\n" + (line ? line + "\n" : "") + TRUNCATION_NOTE;
16782
- }
16783
- cut = MAX_TOOL_RESULT_CHARS - cost - attempt * 16;
16784
- }
16785
- return full.slice(0, MAX_TOOL_RESULT_CHARS) + "\n" + TRUNCATION_NOTE;
16835
+ const projected = project(value, MAX_TOOL_RESULT_CHARS - PROJECTION_SUMMARY_CHARS, "");
16836
+ const shown = projected === void 0 ? {} : JSON.parse(projected);
16837
+ const output = isRecord5(shown) ? shown : { items: shown };
16838
+ return serializeResult({ ...output, __truncated: true, [TRUNCATION_SUMMARY_KEY]: summary });
16786
16839
  }
16787
16840
  function truncateToolResult(details) {
16788
16841
  const annotated = withPartialLists(details);
16789
16842
  const full = serializeResult(annotated);
16790
16843
  if (full.length <= MAX_TOOL_RESULT_CHARS) return full;
16791
- if (full.length > MAX_STRUCTURED_TRUNCATION_CHARS) return markedSlice(full, annotated);
16844
+ if (full.length > MAX_STRUCTURED_TRUNCATION_CHARS) return boundedProjection(full);
16792
16845
  if (Array.isArray(details)) {
16793
16846
  const render2 = (rows) => serializeResult({
16794
16847
  items: rows,
@@ -16801,7 +16854,7 @@ function truncateToolResult(details) {
16801
16854
  if (details && typeof details === "object") {
16802
16855
  const obj = details;
16803
16856
  const arrayKey = largestArrayKey(obj);
16804
- if (!arrayKey) return trimNestedArrays(full) ?? markedSlice(full, annotated);
16857
+ if (!arrayKey) return trimNestedArrays(full) ?? boundedProjection(full);
16805
16858
  const rows = obj[arrayKey];
16806
16859
  const render2 = (kept) => {
16807
16860
  const cursors = {};
@@ -16819,7 +16872,7 @@ function truncateToolResult(details) {
16819
16872
  if (fitsCap(out) && (nested === void 0 || keptRowTotal(nested) <= keptRowTotal(out))) return out;
16820
16873
  if (nested !== void 0) return nested;
16821
16874
  }
16822
- return markedSlice(full, annotated);
16875
+ return boundedProjection(full);
16823
16876
  }
16824
16877
  function textResult2(details, responseSchemas = []) {
16825
16878
  const components = responseSchemas.length > 0 ? responseComponents() : {};
@@ -16829,6 +16882,40 @@ function textResult2(details, responseSchemas = []) {
16829
16882
  details
16830
16883
  };
16831
16884
  }
16885
+ var MEMO_STORED_READS = /* @__PURE__ */ new Set([
16886
+ CanonryMcpToolNames.canonry_competitor_landscape,
16887
+ CanonryMcpToolNames.canonry_measurement_overview,
16888
+ CanonryMcpToolNames.canonry_measurement_portfolio_summary,
16889
+ CanonryMcpToolNames.canonry_measurement_property_evidence,
16890
+ CanonryMcpToolNames.canonry_measurement_property_questions,
16891
+ CanonryMcpToolNames.canonry_measurement_question_result,
16892
+ CanonryMcpToolNames.canonry_measurement_property_competitors,
16893
+ CanonryMcpToolNames.canonry_measurement_changes,
16894
+ CanonryMcpToolNames.canonry_visibility_report,
16895
+ CanonryMcpToolNames.canonry_visibility_stats,
16896
+ CanonryMcpToolNames.canonry_visibility_compare,
16897
+ CanonryMcpToolNames.canonry_sentiment,
16898
+ CanonryMcpToolNames.canonry_sentiment_settings,
16899
+ CanonryMcpToolNames.canonry_sentiment_evidence,
16900
+ CanonryMcpToolNames.canonry_sentiment_compare,
16901
+ CanonryMcpToolNames.canonry_sentiment_backfill_preview,
16902
+ CanonryMcpToolNames.canonry_insights_list
16903
+ ]);
16904
+ var STORED_PAGE_READS = /* @__PURE__ */ new Set([
16905
+ ...MEMO_STORED_READS,
16906
+ CanonryMcpToolNames.canonry_project_overview,
16907
+ CanonryMcpToolNames.canonry_measurement_data_quality,
16908
+ CanonryMcpToolNames.canonry_sentiment_job,
16909
+ CanonryMcpToolNames.canonry_sentiment_jobs
16910
+ ]);
16911
+ var storedReadTools = /* @__PURE__ */ new WeakSet();
16912
+ var storedPageReadTools = /* @__PURE__ */ new WeakSet();
16913
+ function isStoredReadTool(tool) {
16914
+ return storedReadTools.has(tool);
16915
+ }
16916
+ function isStoredPageReadTool(tool) {
16917
+ return storedPageReadTools.has(tool);
16918
+ }
16832
16919
  function stripProjectFromJsonSchema(jsonSchema) {
16833
16920
  if (!jsonSchema || typeof jsonSchema !== "object") {
16834
16921
  return { schema: jsonSchema, hadProject: false };
@@ -16857,17 +16944,20 @@ function mcpToAgentTool(tool, ctx) {
16857
16944
  const result2 = await runWithUsageTags(ctx.client, { mcpTool: tool.name, mcpCall: randomUUID2() }, () => tool.handler(ctx.client, handlerInput));
16858
16945
  return textResult2(result2, responseSchemas);
16859
16946
  };
16860
- return {
16947
+ const adapted = {
16861
16948
  name: tool.name,
16862
16949
  label: tool.title,
16863
16950
  description: tool.description,
16864
16951
  parameters,
16865
16952
  execute
16866
16953
  };
16954
+ if (tool.access === "read" && MEMO_STORED_READS.has(tool.name)) storedReadTools.add(adapted);
16955
+ if (tool.access === "read" && STORED_PAGE_READS.has(tool.name)) storedPageReadTools.add(adapted);
16956
+ return adapted;
16867
16957
  }
16868
16958
  var AERO_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
16869
- // Aero reads stored sentiment. Turning it on and submitting a backfill start
16870
- // paid classifier work, so they stay with the operator (API, CLI, external MCP).
16959
+ // Aero reads stored sentiment. Configuration and backfill submission can
16960
+ // start paid classifier work and remain operator actions.
16871
16961
  "canonry_sentiment_configure",
16872
16962
  "canonry_sentiment_backfill",
16873
16963
  CanonryMcpToolNames.canonry_agent_clear,
@@ -17281,6 +17371,8 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
17281
17371
 
17282
17372
  // src/agent/runtime.ts
17283
17373
  import { z } from "zod";
17374
+ import { randomUUID as randomUUID3 } from "crypto";
17375
+ import { isDeepStrictEqual } from "util";
17284
17376
  import {
17285
17377
  Type as Type4,
17286
17378
  createInitialSystemMessage,
@@ -17291,14 +17383,156 @@ var DISCOVER = "aero_list_toolkits";
17291
17383
  var MAX_VISIBLE_TOOLS = 128;
17292
17384
  var LOAD = "aero_load_toolkit";
17293
17385
  var metadata = new Map(canonryMcpTools.map((tool) => [tool.name, tool]));
17386
+ var PAGE_REFERENCE_PREFIX = "aero-page:";
17387
+ var PAGE_SIZE_PARAMS = /* @__PURE__ */ new Set(["limit", "queryLimit", "attemptLimit"]);
17388
+ var PAGE_CURSORS = [
17389
+ { owner: [], key: "nextCursor", parameter: "cursor" },
17390
+ { owner: ["properties"], key: "nextCursor", parameter: "cursor" },
17391
+ { owner: ["queryPage"], key: "nextCursor", parameter: "queryCursor" },
17392
+ { owner: [], key: "nextAttemptCursor", parameter: "attemptCursor" }
17393
+ ];
17294
17394
  var runtimes = /* @__PURE__ */ new WeakMap();
17295
17395
  var TOOL_LIMIT_WRAP_UP = "This turn has used its tool budget, and no tools are available now. Answer the question from the tool results above: give what they establish with their numbers, say plainly which parts you could not check, and suggest one narrower question the user could ask next.";
17396
+ var TIME_LIMIT_WRAP_UP = TOOL_LIMIT_WRAP_UP.replace("used its tool budget", "used its research time budget");
17296
17397
  function result(value) {
17297
17398
  return { content: [{ type: "text", text: truncateToolResult(value) }], details: value };
17298
17399
  }
17400
+ function pageDefaults(tool, params) {
17401
+ const filled = { ...params };
17402
+ for (const [key, property] of Object.entries(pageOwner(tool.parameters, ["properties"]) ?? {})) {
17403
+ const schema = pageOwner(property, []);
17404
+ if (filled[key] === void 0 && schema && Object.hasOwn(schema, "default")) filled[key] = structuredClone(schema.default);
17405
+ }
17406
+ return filled;
17407
+ }
17408
+ function pageSelection(tool, params, parameter) {
17409
+ const selection = pageDefaults(tool, params);
17410
+ if (tool.name === CanonryMcpToolNames.canonry_measurement_portfolio_summary && selection.list === void 0) {
17411
+ selection.list = MeasurementPortfolioLists["weakest-properties"];
17412
+ }
17413
+ return Object.fromEntries(Object.entries(selection).filter(([key, value]) => key !== parameter && !PAGE_SIZE_PARAMS.has(key) && value !== void 0));
17414
+ }
17415
+ function inheritPageArguments(runtime, message, visible) {
17416
+ for (const block of message.content) {
17417
+ if (block.type !== "toolCall" || !visible.has(block.name)) continue;
17418
+ const tool = runtime.allowed.find((candidate) => candidate.name === block.name);
17419
+ if (!tool || !isStoredPageReadTool(tool) || !pageOwner(block.arguments, [])) continue;
17420
+ let params = block.arguments;
17421
+ for (const parameter of new Set(PAGE_CURSORS.map((cursor) => cursor.parameter))) {
17422
+ const reference = params[parameter];
17423
+ const page = typeof reference === "string" ? runtime.pageReferences.get(reference) : void 0;
17424
+ if (page?.tool === tool && page.parameter === parameter) params = { ...page.params, ...params };
17425
+ }
17426
+ block.arguments = pageDefaults(tool, params);
17427
+ }
17428
+ }
17429
+ function resolvePageReferences(runtime, tool, params) {
17430
+ if (params === null || typeof params !== "object" || Array.isArray(params)) return params;
17431
+ let resolved = params;
17432
+ for (const parameter of new Set(PAGE_CURSORS.map((cursor) => cursor.parameter))) {
17433
+ const value = resolved[parameter];
17434
+ if (typeof value !== "string" || !value.startsWith(PAGE_REFERENCE_PREFIX)) continue;
17435
+ const page = runtime.pageReferences.get(value);
17436
+ if (!page) throw new Error("This Aero page reference is no longer available. Read the first page again with the original filters.");
17437
+ const inherited = { ...page.params, ...resolved };
17438
+ if (page.tool !== tool || page.parameter !== parameter || !isDeepStrictEqual(page.selection, pageSelection(tool, inherited, parameter))) {
17439
+ throw new Error("Use this Aero page reference with the same tool and original filters; only limit, queryLimit or attemptLimit may change.");
17440
+ }
17441
+ resolved = { ...inherited, [parameter]: page.token };
17442
+ }
17443
+ return resolved;
17444
+ }
17445
+ function pageOwner(value, path9) {
17446
+ let owner = value;
17447
+ for (const key of path9) {
17448
+ if (owner === null || typeof owner !== "object" || Array.isArray(owner)) return void 0;
17449
+ owner = owner[key];
17450
+ }
17451
+ return owner !== null && typeof owner === "object" && !Array.isArray(owner) ? owner : void 0;
17452
+ }
17453
+ function referencePages(runtime, tool, params, value) {
17454
+ if (!isStoredPageReadTool(tool) || value.isError || value.content.length !== 1 || params === null || typeof params !== "object" || Array.isArray(params)) return value;
17455
+ const block = value.content[0];
17456
+ if (block.type !== "text") return value;
17457
+ let shown;
17458
+ try {
17459
+ shown = JSON.parse(block.text);
17460
+ } catch {
17461
+ return value;
17462
+ }
17463
+ const root = pageOwner(shown, []);
17464
+ if (!root || root.__truncated === true) return value;
17465
+ let changed = false;
17466
+ for (const cursor of PAGE_CURSORS) {
17467
+ const owner = pageOwner(root, cursor.owner);
17468
+ const token = owner?.[cursor.key];
17469
+ if (!Object.hasOwn(pageOwner(tool.parameters, ["properties"]) ?? {}, cursor.parameter) || !owner || owner.__truncated === true || typeof token !== "string" || token !== pageOwner(value.details, cursor.owner)?.[cursor.key]) continue;
17470
+ if (runtime.pageReferences.size >= runtime.limits.maxToolCalls * PAGE_CURSORS.length) break;
17471
+ const reference = `${PAGE_REFERENCE_PREFIX}${runtime.pageNamespace}-${runtime.pageTurn}:${runtime.pageReferences.size + 1}`;
17472
+ if (token.length <= reference.length) continue;
17473
+ runtime.pageReferences.set(reference, {
17474
+ tool,
17475
+ token,
17476
+ parameter: cursor.parameter,
17477
+ params: structuredClone(params),
17478
+ selection: structuredClone(pageSelection(tool, params, cursor.parameter))
17479
+ });
17480
+ owner[cursor.key] = reference;
17481
+ const pagination = pageOwner(root.__pagination, []);
17482
+ for (const [path9, note] of Object.entries(pagination ?? {})) {
17483
+ if (typeof note === "string") pagination[path9] = note.split(token).join(reference);
17484
+ }
17485
+ changed = true;
17486
+ }
17487
+ if (!changed) return value;
17488
+ root.__pageReferences = "Aero page references expire at the end of this turn. Pass them unchanged to the same tool. Omitted filters inherit the original scope; supplied filters must match. Only the page size may change.";
17489
+ let text = JSON.stringify(root);
17490
+ if (text.length > block.text.length) {
17491
+ delete root.__pageReferences;
17492
+ text = JSON.stringify(root);
17493
+ }
17494
+ return { ...value, content: [{ ...block, text }] };
17495
+ }
17299
17496
  function visibleTools(runtime) {
17300
- if (!runtime.progressive) return runtime.allowed;
17301
- const visible = runtime.allowed.filter((tool) => {
17497
+ const allowed = runtime.allowed.map((tool) => ({
17498
+ ...tool,
17499
+ execute: async (id, params, signal, onUpdate) => {
17500
+ const dispatchedParams = resolvePageReferences(runtime, tool, params);
17501
+ const safeRead = isStoredReadTool(tool);
17502
+ const key = safeRead ? `${tool.name}:${JSON.stringify(dispatchedParams)}` : void 0;
17503
+ const prior = key === void 0 ? void 0 : runtime.memo.get(key);
17504
+ if (prior) {
17505
+ const previous = await prior.value;
17506
+ if (!previous.isError) return result({ alreadyReturned: true, toolCallId: prior.id, note: "Already returned above. Use that result and follow its pagination instruction. A cap-cut page must be re-requested from the original cursor with a smaller limit." });
17507
+ }
17508
+ if (!safeRead) runtime.memo.clear();
17509
+ const research = runtime.research?.signal;
17510
+ const signals = [signal, research].filter((value) => value !== void 0);
17511
+ const combined = signals.length > 0 ? AbortSignal.any(signals) : void 0;
17512
+ let interrupted;
17513
+ const work = Promise.race([
17514
+ tool.execute(id, dispatchedParams, combined, onUpdate),
17515
+ new Promise((_resolve, reject) => {
17516
+ interrupted = () => reject(new Error("Research time budget elapsed; dispatched work may still settle."));
17517
+ research?.addEventListener("abort", interrupted, { once: true });
17518
+ if (research?.aborted) interrupted();
17519
+ })
17520
+ ]).then((value) => referencePages(runtime, tool, dispatchedParams, value)).finally(() => {
17521
+ if (interrupted) research?.removeEventListener("abort", interrupted);
17522
+ });
17523
+ if (key !== void 0) runtime.memo.set(key, { id, value: work });
17524
+ try {
17525
+ const value = await work;
17526
+ if (key !== void 0 && value.isError) runtime.memo.delete(key);
17527
+ return value;
17528
+ } catch (error) {
17529
+ if (key !== void 0) runtime.memo.delete(key);
17530
+ throw error;
17531
+ }
17532
+ }
17533
+ }));
17534
+ if (!runtime.progressive) return allowed;
17535
+ const visible = allowed.filter((tool) => {
17302
17536
  const entry = metadata.get(tool.name);
17303
17537
  return !entry || entry.tier === "core" || runtime.pinned.has(tool.name) || runtime.loaded.has(entry.tier);
17304
17538
  });
@@ -17379,11 +17613,26 @@ function correctToolNames(runtime, message, visible) {
17379
17613
  block.name = matches[0];
17380
17614
  }
17381
17615
  }
17382
- function withCorrectedToolNames(runtime, response, visible) {
17616
+ function withCorrectedToolNames(runtime, response, visible, wrapUp, signal) {
17383
17617
  const result2 = response.result.bind(response);
17384
17618
  response.result = async () => {
17385
17619
  const message = await result2();
17620
+ const researchEnded = !wrapUp && runtime.research?.signal.aborted && !signal?.aborted;
17621
+ const canFinish = wrapUp && (!signal?.aborted || runtime.hardDeadline);
17622
+ const failedAnswer = canFinish && !runtime.hardDeadline && (message.stopReason === "error" || message.stopReason === "aborted");
17623
+ if (researchEnded) {
17624
+ message.content = message.content.filter((block) => block.type !== "toolCall");
17625
+ if (!message.content.some((block) => block.type === "text" && block.text.trim())) message.content.push({ type: "text", text: "Research time budget elapsed; preparing an answer from the evidence already gathered." });
17626
+ message.stopReason = "stop";
17627
+ delete message.errorMessage;
17628
+ } else if (canFinish && (message.stopReason === "error" || message.stopReason === "aborted" || !message.content.some((block) => block.type === "text" && block.text.trim()))) {
17629
+ message.content.push({ type: "text", text: `I reached this turn's ${runtime.reason === "time-limit" ? "time" : "tool"} limit and could not complete the answer. The results above are partial; ask a narrower question to continue.` });
17630
+ message.stopReason = "stop";
17631
+ delete message.errorMessage;
17632
+ }
17633
+ if (failedAnswer) runtime.reason = "error";
17386
17634
  correctToolNames(runtime, message, visible);
17635
+ inheritPageArguments(runtime, message, visible);
17387
17636
  return message;
17388
17637
  };
17389
17638
  return response;
@@ -17417,7 +17666,7 @@ function explainMissingTool(runtime, message) {
17417
17666
  function configureAeroRuntime(agent, allowed, limits, progressive = true, pinned = []) {
17418
17667
  let runtime = runtimes.get(agent);
17419
17668
  if (!runtime) {
17420
- runtime = { allowed, loaded: /* @__PURE__ */ new Set(), limits: agentTurnLimitsSchema.parse(limits ?? {}), calls: 0, rounds: 0, startedAt: 0, reason: "completed", progressive, pinned: new Set(pinned), corrected: /* @__PURE__ */ new Map(), wrapUp: false };
17669
+ runtime = { allowed, loaded: /* @__PURE__ */ new Set(), limits: agentTurnLimitsSchema.parse(limits ?? {}), calls: 0, rounds: 0, startedAt: 0, reason: "completed", progressive, pinned: new Set(pinned), corrected: /* @__PURE__ */ new Map(), wrapUp: false, hardDeadline: false, memo: /* @__PURE__ */ new Map(), pageReferences: /* @__PURE__ */ new Map(), pageNamespace: randomUUID3().slice(0, 8), pageTurn: 0 };
17421
17670
  runtimes.set(agent, runtime);
17422
17671
  const state = runtime;
17423
17672
  const stream = agent.streamFunction;
@@ -17425,29 +17674,39 @@ function configureAeroRuntime(agent, allowed, limits, progressive = true, pinned
17425
17674
  options?.signal?.throwIfAborted();
17426
17675
  state.rounds++;
17427
17676
  const visible = new Set(getCurrentTools(context.messages).map((tool) => tool.name));
17428
- const response = stream(model, context, options);
17429
- return response instanceof Promise ? response.then((ready) => withCorrectedToolNames(state, ready, visible)) : withCorrectedToolNames(state, response, visible);
17677
+ const wrapUp = state.wrapUp;
17678
+ const signals = [options?.signal, wrapUp ? void 0 : state.research?.signal].filter((value) => value !== void 0);
17679
+ const messages = [...context.messages];
17680
+ let clockIndex = 0;
17681
+ while (messages[clockIndex]?.role === "system") clockIndex++;
17682
+ messages.splice(clockIndex, 0, {
17683
+ role: "system",
17684
+ timestamp: state.startedAt,
17685
+ content: `Current turn clock (UTC): ${new Date(state.startedAt).toISOString()}. Use this current UTC date and year for relative dates or omitted years unless the user explicitly specifies a year. Stored run and scan dates remain authoritative; this clock is not an observation date.`
17686
+ });
17687
+ const response = stream(model, { ...context, messages }, { ...options, ...signals.length > 0 ? { signal: AbortSignal.any(signals) } : {} });
17688
+ return response instanceof Promise ? response.then((ready) => withCorrectedToolNames(state, ready, visible, wrapUp, options?.signal)) : withCorrectedToolNames(state, response, visible, wrapUp, options?.signal);
17430
17689
  };
17431
17690
  const before = agent.beforeToolCall;
17432
17691
  const after = agent.afterToolCall;
17433
17692
  const overLimit = /* @__PURE__ */ new Set();
17434
17693
  agent.beforeToolCall = async (event, signal) => {
17435
- if (signal?.aborted || overLimit.has(event.toolCall.id)) return { block: true, reason: "Turn stopped." };
17694
+ if (signal?.aborted || state.reason === "time-limit" || state.wrapUp || overLimit.has(event.toolCall.id)) return { block: true, reason: "Turn stopped." };
17436
17695
  return before?.(event, signal);
17437
17696
  };
17438
17697
  const finish = agent.finishTurn;
17439
17698
  agent.finishTurn = (turn, signal) => {
17440
- if (state.reason !== "tool-limit") return finish?.(turn, signal);
17699
+ if (state.reason !== "tool-limit" && state.reason !== "time-limit") return finish?.(turn, signal);
17441
17700
  if (state.wrapUp) return { action: "end" };
17442
17701
  state.wrapUp = true;
17443
17702
  return { action: "continue" };
17444
17703
  };
17445
17704
  const prepare = agent.prepareNextTurnWithContext;
17446
17705
  agent.prepareNextTurnWithContext = (turn, signal) => {
17447
- if (state.reason === "tool-limit" && state.wrapUp) {
17706
+ if ((state.reason === "tool-limit" || state.reason === "time-limit") && state.wrapUp) {
17448
17707
  return {
17449
17708
  context: { ...turn.context, tools: [] },
17450
- messages: [{ role: "system", content: TOOL_LIMIT_WRAP_UP, timestamp: Date.now() }]
17709
+ messages: [{ role: "system", content: state.reason === "time-limit" ? TIME_LIMIT_WRAP_UP : TOOL_LIMIT_WRAP_UP, timestamp: Date.now() }]
17451
17710
  };
17452
17711
  }
17453
17712
  return prepare ? prepare(turn, signal) : agent.prepareNextTurn?.(signal);
@@ -17466,7 +17725,7 @@ function configureAeroRuntime(agent, allowed, limits, progressive = true, pinned
17466
17725
  agent.subscribe((event) => {
17467
17726
  if (event.type === "tool_execution_start") {
17468
17727
  if (state.calls >= state.limits.maxToolCalls) {
17469
- state.reason = "tool-limit";
17728
+ if (state.reason !== "time-limit") state.reason = "tool-limit";
17470
17729
  overLimit.add(event.toolCallId);
17471
17730
  } else if (!agent.signal?.aborted) {
17472
17731
  state.calls++;
@@ -17497,24 +17756,41 @@ function configureAeroRuntime(agent, allowed, limits, progressive = true, pinned
17497
17756
  overLimit.clear();
17498
17757
  state.corrected.clear();
17499
17758
  state.wrapUp = false;
17759
+ state.hardDeadline = false;
17760
+ state.memo.clear();
17761
+ state.pageReferences.clear();
17762
+ state.pageTurn++;
17763
+ state.research = new AbortController();
17500
17764
  state.calls = 0;
17501
17765
  state.rounds = 0;
17502
17766
  state.reason = "completed";
17503
17767
  state.startedAt = Date.now();
17504
17768
  state.finishedAt = void 0;
17769
+ const reservedMs = Math.min(15e3, Math.max(250, Math.floor(state.limits.timeoutMs / 10)));
17505
17770
  state.timer = setTimeout(() => {
17771
+ if (state.wrapUp) return;
17772
+ state.reason = "time-limit";
17773
+ state.research?.abort();
17774
+ }, state.limits.timeoutMs - reservedMs);
17775
+ state.deadlineTimer = setTimeout(() => {
17506
17776
  state.reason = "time-limit";
17777
+ state.hardDeadline = true;
17507
17778
  agent.abort();
17508
17779
  }, state.limits.timeoutMs);
17509
17780
  state.timer.unref();
17781
+ state.deadlineTimer.unref();
17510
17782
  } else if (event.type === "agent_end") {
17511
17783
  state.finishedAt = Date.now();
17512
17784
  clearTimeout(state.timer);
17513
- if ((state.reason === "completed" || state.reason === "tool-limit") && agent.signal?.aborted) state.reason = "stopped";
17514
- if ((state.reason === "completed" || state.reason === "tool-limit") && agent.state.errorMessage) state.reason = "error";
17785
+ clearTimeout(state.deadlineTimer);
17786
+ state.memo.clear();
17787
+ state.pageReferences.clear();
17788
+ if (agent.signal?.aborted && !state.hardDeadline) state.reason = "stopped";
17789
+ if (state.reason === "completed" && agent.state.errorMessage) state.reason = "error";
17515
17790
  }
17516
17791
  });
17517
17792
  }
17793
+ runtime.pageReferences.clear();
17518
17794
  runtime.allowed = allowed;
17519
17795
  runtime.loaded = /* @__PURE__ */ new Set();
17520
17796
  runtime.limits = agentTurnLimitsSchema.parse(limits ?? {});
@@ -17823,11 +18099,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17823
18099
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
17824
18100
  var log21 = createLogger("RemoteMcp");
17825
18101
  var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
17826
- var MAX_TOOL_RESULT_CHARS2 = 2e4;
17827
- function truncate(text) {
17828
- if (text.length <= MAX_TOOL_RESULT_CHARS2) return text;
17829
- return text.slice(0, MAX_TOOL_RESULT_CHARS2) + "\n... (truncated, result too large)";
17830
- }
17831
18102
  async function connectStreamableHttp(server) {
17832
18103
  const transport = new StreamableHTTPClientTransport(new URL(server.url), {
17833
18104
  requestInit: {
@@ -17853,8 +18124,23 @@ function adaptRemoteTool(client, tool) {
17853
18124
  );
17854
18125
  const execute = async (_toolCallId, params) => {
17855
18126
  const result2 = await client.callTool({ name: tool.name, arguments: params });
18127
+ let shown = result2;
18128
+ if (result2 && typeof result2 === "object") {
18129
+ const envelope = result2;
18130
+ if (envelope.structuredContent !== void 0) shown = envelope.structuredContent;
18131
+ else if (envelope.content?.length === 1 && envelope.content[0]?.type === "text") {
18132
+ const text = envelope.content[0].text ?? "";
18133
+ try {
18134
+ shown = JSON.parse(text);
18135
+ } catch {
18136
+ shown = text;
18137
+ }
18138
+ } else if (envelope.content?.some((block) => block.type === "text")) {
18139
+ shown = { ...result2, content: Object.fromEntries(envelope.content.map((block, index) => [`block_${index + 1}`, block])) };
18140
+ }
18141
+ }
17856
18142
  return {
17857
- content: [{ type: "text", text: truncate(JSON.stringify(result2, null, 2)) }],
18143
+ content: [{ type: "text", text: truncateToolResult(shown) }],
17858
18144
  details: result2,
17859
18145
  isError: isRemoteToolError(result2)
17860
18146
  };
@@ -18027,10 +18313,10 @@ var ADVANCED_TOOLS = [
18027
18313
  var LEGACY_TOOLS = ["canonry_measurement_overview", "canonry_measurement_plan_get", "canonry_run_completeness"];
18028
18314
  var SIMPLE_TOOLS = ["canonry_visibility_report"];
18029
18315
  var ADVANCED_ROUTES = [
18030
- "Weakest or strongest Properties: canonry_measurement_portfolio_summary (rows with metro, names written instead and cited domains; mentionRanking for both ends). For more rows pass groupKey, or page canonry_measurement_overview.",
18031
- "Which metros have the biggest gaps: the summary's markets (every metro) and tiedAtWeakest.byMetro.",
18316
+ "Weakest or strongest Properties: canonry_measurement_portfolio_summary (mentionRanking for both ends). Compact results carry detailsOmitted; use Property evidence reads for omitted names and cited domains. The default cursor enumerates weakest Properties only; set list=strongest-mentions or list=weakest-mentions to enumerate the chosen ranking. Follow nextCursor with unchanged list and filters; pageList names that selection. Use groupKey or page canonry_measurement_overview for all Property rows.",
18317
+ "Which metros have the biggest gaps: the compact summary's weakestMarkets, whose rates cover full top-level metros. Set list=markets and follow nextCursor with unchanged list and filters for every market; tiedAtWeakest describes only the zero-signal Property cohort, never full metro rates.",
18032
18318
  "One Property: canonry_measurement_property_evidence and canonry_measurement_property_competitors (names written instead of it, and citedDomains, its own cited sources). Portfolio lists such as weakestAnswerSources pool many Properties.",
18033
- 'Which names answers give instead across the portfolio: canonry_competitor_landscape with queryClass and runId "latest". Per-Property named-instead lists are samples of weak Properties.',
18319
+ 'Which names answers give instead across the portfolio: canonry_competitor_landscape with queryClass and runId "latest", answers=not-mentioned. This selects answers naming none of their targeted Properties; preserve populationSize, answerCount and unknownMentionAnswers. Per-Property named-instead lists are samples of weak Properties.',
18034
18320
  'Where answers get their sources: canonry_analytics_sources with queryClass and runId "latest"; without them it pools both classes and every sweep.',
18035
18321
  "What changed since the last sweep: canonry_measurement_changes once per class. Quote its distribution for how many Properties moved.",
18036
18322
  "Is the sweep complete, or is anything unreliable: canonry_measurement_data_quality (quote completeness expected, executed and missing, unattributedByClass and latestFill), then canonry_run_completeness with its run.displayedRunId for missing answers per engine. A Healthy run status and canonry_doctor are not completeness checks."
@@ -18060,7 +18346,7 @@ Project shape: an Advanced Measurement plan on the legacy schema v1 (plan revisi
18060
18346
  prompt: `
18061
18347
 
18062
18348
  Project shape: an Advanced Measurement portfolio (plan revision ${version.revision}) with ${size}, ${queriesIn("branded")} branded and ${queriesIn("non-brand")} non-brand queries. This line settles the project type.
18063
- Rules: measure per Property, group or market, never pool branded and non-brand, and a query shared by several Properties is one answer scored for each. Denominators count answers (queries x engines), not queries; label them answers. Group Properties only by the metro and submarkets the tools return, never by name. Names given instead were written in the answer text, not cited; cited domains are sources and never go under a named-instead heading. Name only engines a tool returned. Between two sweeps, a Property that moved ${MEASUREMENT_CHANGES_NOISE_ANSWERS} answers or fewer is within noise (withinNoise): never call it a gain, loss, trend or regression. Judge mention and citation separately (mentionWithinNoise, citationWithinNoise): a signal that moved ${MEASUREMENT_CHANGES_NOISE_ANSWERS} answers or fewer is within noise even when the Property's other signal moved more. Quote every coverage figure with its counts, headline and branded figures included; the value text carries them, as in 25.0% (6/24). A Property's citation coverage counts only citations of its own page: 0% does not mean your domain went uncited (weakestAnswerSources.ownDomainAnswers counts that). Property names often carry the brand, so that most listed Properties carry it is not a finding. Properties tied at the weakest rate are not ranked: they are listed by name, so say how many tie (tiedAtWeakest.count), give tiedAtWeakest.byMetro, and call the listed rows examples of the tie, not a ranked bottom list. A result with truncated true, a total above its rows, __partialLists or __truncation is partial: say how many of how many you saw, and never call those rows the biggest, all, or the full picture.
18349
+ Rules: measure per Property, group or market, never pool branded and non-brand, and a query shared by several Properties is one answer scored for each. Denominators count answers (queries x engines), not queries; label them answers. Group Properties only by the metro and submarkets the tools return, never by name. Names given instead were written in the answer text, not cited; cited domains are sources and never go under a named-instead heading. Name only engines a tool returned. Between two sweeps, a Property that moved ${MEASUREMENT_CHANGES_NOISE_ANSWERS} answers or fewer is within noise (withinNoise): never call it a gain, loss, trend or regression. Judge mention and citation separately (mentionWithinNoise, citationWithinNoise): a signal that moved ${MEASUREMENT_CHANGES_NOISE_ANSWERS} answers or fewer is within noise even when the Property's other signal moved more. Quote every coverage figure with its counts, headline and branded figures included; the value text carries them, as in 25.0% (6/24). A Property's citation coverage counts only citations of its own page: 0% does not mean your domain went uncited (weakestAnswerSources.ownDomainAnswers counts that). Property names often carry the brand, so that most listed Properties carry it is not a finding. tiedAtWeakest counts Properties with zero mentions and zero citations, separate from propertiesNeverMentioned (which may include cited Properties). Say how many tie (tiedAtWeakest.count); compact results omit the cohort's byMetro details. Call listed tie rows examples, not a ranked bottom list. A result with truncated true, a total above its rows, __partialLists or __truncation is partial: say how many of how many you saw, and never call those rows the biggest, all, or the full picture.
18064
18350
  Route each question to its read:
18065
18351
  ${ADVANCED_ROUTES.map((route) => `- ${route}`).join("\n")}
18066
18352
  canonry_visibility_report describes the whole project, not one Property. canonry_measurement_plan_get is plan structure with no metrics; do not read it for analysis.`,
@@ -21039,7 +21325,7 @@ function cloneGtmConfig(config) {
21039
21325
  function createGoogleMarketingConfigCredentialStore(input) {
21040
21326
  const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
21041
21327
  const env = input.env ?? process.env;
21042
- const randomUUID3 = input.randomUUID ?? crypto29.randomUUID;
21328
+ const randomUUID4 = input.randomUUID ?? crypto29.randomUUID;
21043
21329
  const snapshot = () => ({
21044
21330
  googleAds: cloneGoogleAdsConfig(input.config.googleAds),
21045
21331
  gtm: cloneGtmConfig(input.config.gtm)
@@ -21087,7 +21373,7 @@ function createGoogleMarketingConfigCredentialStore(input) {
21087
21373
  upsertGoogleAdsConnection(input.config, {
21088
21374
  projectId: project.id,
21089
21375
  projectName: project.name,
21090
- credentialGeneration: randomUUID3(),
21376
+ credentialGeneration: randomUUID4(),
21091
21377
  ...credential.accessToken ? { accessToken: credential.accessToken } : {},
21092
21378
  refreshToken: credential.refreshToken ?? null,
21093
21379
  tokenExpiresAt: credential.expiresAt ?? null,
@@ -21099,7 +21385,7 @@ function createGoogleMarketingConfigCredentialStore(input) {
21099
21385
  upsertGtmConnection(input.config, {
21100
21386
  projectId: project.id,
21101
21387
  projectName: project.name,
21102
- credentialGeneration: randomUUID3(),
21388
+ credentialGeneration: randomUUID4(),
21103
21389
  ...credential.accessToken ? { accessToken: credential.accessToken } : {},
21104
21390
  refreshToken: credential.refreshToken ?? null,
21105
21391
  tokenExpiresAt: credential.expiresAt ?? null,