@ai-matrx/agents 0.59.264 → 0.59.266

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.59.266
4
+ ### model labels by id (FW-014 round 3)
5
+
6
+ - `ModelRecords.loadIdentities(modelIds)`: the durable labels of several models in ONE
7
+ `ai.model_definition … in(id)` read, deduped with `loadIdentity` — what an open menu names without the
8
+ options list (`ai.model_offering` + `ai.model_public`). Consumer action: a custom `ModelRecords` fake
9
+ adds `loadIdentities`.
10
+
11
+
12
+ ## 0.59.265
13
+ ### tagged rows without the catalog (FW-014, composer)
14
+
15
+ - `ensureTaggedRows(tags)` + `readAgentTaggedRows` (`agent.agx_picker_tagged`, live 2026-10-23, reach verified
16
+ against `agx_get_list_full`): the caller's live agents carrying any tag, never the whole catalog.
17
+ Consumer action: none.
18
+
19
+ ### the agent menu opens on one thin page, never the catalog (FW-014)
20
+
21
+ - Every picker (`AgentListDropdown`, `AgentListPanel`, `AgentListInlinePicker`, native `AgentPickerBody`)
22
+ now opens on ONE page of `agent.agx_picker_page` — favorites, then the agents the person used most
23
+ recently, then name; 50 rows; only the columns a row renders; the tab counts on page 1 — and reads the
24
+ next page on scroll. Typing still searches `agx_search`. Measured in SQL: page 1 3.4 KB (ordinary account)
25
+ / 32 KB (admin) in ~0.2–0.4 s, where opening the menu used to read 673 / 1,476 full rows (~340 KB, 2.6 s).
26
+ The whole catalog is read only when the person opens the category/tag facets, picks another sort, a
27
+ favorites/access filter, or the host passes `agentFilter` — or when another surface already holds it fresh.
28
+ - New on the catalog: `ensurePickerPage(query)`, `loadMorePickerPage(query)`, `ensureAgentRows(ids)` (full
29
+ rows by id through `agent.agx_get_by_ids`), `state.pickerPages`, `agentPickerPageKey`, `pickerReads`.
30
+ New data doors: `readAgentPickerPage`, `readAgentRowsByIds`, `PICKER_PAGE_SIZE`. `AgentSummary` gains
31
+ `isThin` (a page row: no description/model/offering/task/source/sharer) and `lastUsedAt`.
32
+ - A thin row never replaces a full one; hovering a thin row reads that one row (web and native detail
33
+ cards render the full row when it lands); the pinned current agent is read by id when it is not on page 1.
34
+ - `useAgentSummary` reads the one row by id (`ensureAgentRows`) instead of loading the whole catalog;
35
+ server search remains the fallback for an id that read does not return.
36
+ - `useAgentListCore` returns `paged`, `hasMore`, `loadingMore`, `loadMore`, `totalCount`,
37
+ `ensureFullCatalog`; `AgentListContent` (web) takes `hasMore` / `onReachEnd` / `loadingMore` /
38
+ `totalCount`, native takes `onReachEnd` / `totalCount`.
39
+ - Knob: `createAgentCatalog({ pickerReads: "catalog" })` restores the read-everything-on-open behavior (a
40
+ host whose client cannot reach the `agent` schema; tests of the full-list rules). Default `"page"`.
41
+ - Consumer action: none for hosts using supabase-js (needs `agent.agx_picker_page`, live since
42
+ 2026-10-23). A host that reads `catalog.getState().rows` expecting the whole catalog after a picker
43
+ opened must call `ensureLoaded()` itself — the picker no longer loads it. A host component that
44
+ renders a hovered agent's description from the picker's object should read the store row
45
+ (`getAgent(id)`), which `ensureAgentRows` fills.
46
+
47
+ ### no second star read after a catalog or search read (FW-014)
3
48
  ## 0.59.264
49
+
50
+ Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 6201da503d aa81498da0 -- apps/shared/matrx-agents/src`
51
+ has no shipped-source changes outside tests, fixtures and docs). No consumer action required.
52
+
53
+ ## 0.59.263
54
+
55
+ Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff de2b958734 6201da503d -- apps/shared/matrx-agents/src`
56
+ has no shipped-source changes outside tests, fixtures and docs). No consumer action required.
57
+
58
+ ## 0.59.262
4
59
  ### no second star read after a catalog or search read (FW-014)
5
60
 
6
61
  - `ensureLoaded()` and `searchServer()` no longer follow each read with `ues_get_bulk('agent', every id)`.
@@ -256,7 +256,8 @@ function toAgentSummary(raw) {
256
256
  orchestra: orchestraBadge(raw["orchestra"]),
257
257
  // Present only when the read carries the column: absent ≠ "no pin".
258
258
  ..."offering_id" in raw ? { offeringId: str(raw["offering_id"]) } : {},
259
- ..."organization_name" in raw ? { organizationName: str(raw["organization_name"]) } : {}
259
+ ..."organization_name" in raw ? { organizationName: str(raw["organization_name"]) } : {},
260
+ ..."last_used_at" in raw ? { lastUsedAt: str(raw["last_used_at"]) } : {}
260
261
  };
261
262
  }
262
263
 
@@ -395,20 +396,77 @@ async function readFavoriteAgentRows(client, favorites) {
395
396
  )
396
397
  );
397
398
  if (ids.length === 0) return [];
399
+ const rows = await readAgentRowsByIds(client, ids, "agent.agx_get_by_ids (pinned)");
400
+ return rows.map((summary) => summary.isFavorite ? summary : { ...summary, isFavorite: true });
401
+ }
402
+ async function readAgentRowsByIds(client, agentIds, label = "agent.agx_get_by_ids") {
403
+ if (typeof client.schema !== "function") {
404
+ throw new AgentPinnedReadUnsupported("the client has no schema()");
405
+ }
406
+ const ids = Array.from(new Set(agentIds.filter((id) => typeof id === "string" && id.length > 0)));
398
407
  const rows = [];
399
408
  for (let at = 0; at < ids.length; at += PINNED_ID_CHUNK) {
400
409
  const chunk = ids.slice(at, at + PINNED_ID_CHUNK);
401
410
  const { data, error } = await client.schema("agent").rpc("agx_get_by_ids", { p_ids: chunk });
402
411
  if (error) {
403
- throw new AgentCatalogReadError("agent.agx_get_by_ids (pinned)", pgDetail(error));
412
+ throw new AgentCatalogReadError(label, pgDetail(error));
404
413
  }
405
414
  for (const item of Array.isArray(data) ? data : []) {
406
415
  const summary = toAgentSummary(item);
407
- if (summary) rows.push(summary.isFavorite ? summary : { ...summary, isFavorite: true });
416
+ if (summary) rows.push(summary);
408
417
  }
409
418
  }
410
419
  return rows;
411
420
  }
421
+ var PICKER_PAGE_SIZE = 50;
422
+ function countOf(record, key) {
423
+ const value = record[key];
424
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
425
+ }
426
+ async function readAgentPickerPage(client, query, cursor = null, limit = PICKER_PAGE_SIZE) {
427
+ if (typeof client.schema !== "function") {
428
+ throw new AgentPinnedReadUnsupported("the client has no schema()");
429
+ }
430
+ const { data, error } = await client.schema("agent").rpc("agx_picker_page", {
431
+ p_tab: query.tab,
432
+ p_archive: query.archive,
433
+ p_include_quick: query.includeQuick,
434
+ p_cursor: cursor ?? null,
435
+ p_limit: limit
436
+ });
437
+ if (error) throw new AgentCatalogReadError("agent.agx_picker_page", pgDetail(error));
438
+ const record = typeof data === "object" && data !== null && !Array.isArray(data) ? data : null;
439
+ if (!record) {
440
+ throw new AgentCatalogReadError("agent.agx_picker_page", "the page came back without a body");
441
+ }
442
+ const rows = [];
443
+ for (const item of Array.isArray(record["rows"]) ? record["rows"] : []) {
444
+ const summary = toAgentSummary(item);
445
+ if (summary) rows.push({ ...summary, isThin: true });
446
+ }
447
+ const rawCounts = record["counts"];
448
+ const counts = typeof rawCounts === "object" && rawCounts !== null && !Array.isArray(rawCounts) ? {
449
+ mine: countOf(rawCounts, "mine"),
450
+ shared: countOf(rawCounts, "shared"),
451
+ all: countOf(rawCounts, "all"),
452
+ system: countOf(rawCounts, "system"),
453
+ archivedHidden: countOf(rawCounts, "archived_hidden")
454
+ } : null;
455
+ return { rows, nextCursor: record["next_cursor"] ?? null, counts };
456
+ }
457
+ async function readAgentTaggedRows(client, tags) {
458
+ if (typeof client.schema !== "function") {
459
+ throw new AgentPinnedReadUnsupported("the client has no schema()");
460
+ }
461
+ const { data, error } = await client.schema("agent").rpc("agx_picker_tagged", { p_tags: [...tags] });
462
+ if (error) throw new AgentCatalogReadError("agent.agx_picker_tagged", pgDetail(error));
463
+ const rows = [];
464
+ for (const item of Array.isArray(data) ? data : []) {
465
+ const summary = toAgentSummary(item);
466
+ if (summary) rows.push({ ...summary, isThin: true });
467
+ }
468
+ return rows;
469
+ }
412
470
 
413
471
  // catalog/default-row.ts
414
472
  var CACHE_PREFIX = "ai-matrx.agents.catalog.default-row:";
@@ -746,6 +804,9 @@ function readsOrchestraColumn(data) {
746
804
  // catalog/store.ts
747
805
  var LIST_TTL_MS = 15 * 60 * 1e3;
748
806
  var LIST_STALE_MS = 4 * 60 * 60 * 1e3;
807
+ function agentPickerPageKey(query) {
808
+ return `${query.tab}|${query.archive}|${query.includeQuick ? "q" : "-"}`;
809
+ }
749
810
  function createDefaultErrorSink() {
750
811
  let announced = false;
751
812
  return (event) => {
@@ -829,7 +890,8 @@ function createAgentCatalog(config) {
829
890
  error: null,
830
891
  consumers: {},
831
892
  defaultRows: {},
832
- favoriteRowsStatus: "idle"
893
+ favoriteRowsStatus: "idle",
894
+ pickerPages: {}
833
895
  };
834
896
  const listeners = /* @__PURE__ */ new Set();
835
897
  function publish(next) {
@@ -839,7 +901,10 @@ function createAgentCatalog(config) {
839
901
  function mergeRows(incoming) {
840
902
  if (incoming.length === 0) return;
841
903
  const byId = { ...state.byId };
842
- for (const row of incoming) byId[row.id] = row;
904
+ for (const row of incoming) {
905
+ const held = byId[row.id];
906
+ byId[row.id] = row.isThin && held && !held.isThin ? { ...held, isFavorite: row.isFavorite, ...row.lastUsedAt !== void 0 ? { lastUsedAt: row.lastUsedAt } : {} } : row;
907
+ }
843
908
  publish({ ...state, byId, rows: Object.values(byId) });
844
909
  }
845
910
  function replaceRows(incoming) {
@@ -977,6 +1042,164 @@ function createAgentCatalog(config) {
977
1042
  favoriteRowsInFlight = run;
978
1043
  return run;
979
1044
  }
1045
+ const pickerInFlight = /* @__PURE__ */ new Map();
1046
+ function writePickerPage(key, next) {
1047
+ publish({ ...state, pickerPages: { ...state.pickerPages, [key]: next } });
1048
+ }
1049
+ async function readPickerPage(query, first) {
1050
+ const key = agentPickerPageKey(query);
1051
+ const prior = state.pickerPages[key];
1052
+ if (getUserIdOrNull() === null) {
1053
+ writePickerPage(key, { ids: [], nextCursor: null, done: true, status: "succeeded", loadingMore: false, error: null, counts: { mine: 0, shared: 0, all: 0, system: 0, archivedHidden: 0 }, fetchedAt: Date.now() });
1054
+ return;
1055
+ }
1056
+ const base = {
1057
+ ids: [],
1058
+ nextCursor: null,
1059
+ done: false,
1060
+ status: "loading",
1061
+ loadingMore: false,
1062
+ error: null,
1063
+ counts: null,
1064
+ fetchedAt: 0
1065
+ };
1066
+ if (first) {
1067
+ if (!prior || prior.status === "failed") writePickerPage(key, { ...prior ?? base, status: "loading", error: null });
1068
+ } else if (prior) {
1069
+ writePickerPage(key, { ...prior, loadingMore: true, error: null });
1070
+ }
1071
+ try {
1072
+ const page = await readAgentPickerPage(client, query, first ? null : prior?.nextCursor ?? null);
1073
+ mergeRows(page.rows);
1074
+ const current = state.pickerPages[key];
1075
+ const keptIds = first ? [] : current?.ids ?? [];
1076
+ const seen = new Set(keptIds);
1077
+ const ids = [...keptIds];
1078
+ for (const row of page.rows) if (!seen.has(row.id)) {
1079
+ seen.add(row.id);
1080
+ ids.push(row.id);
1081
+ }
1082
+ writePickerPage(key, {
1083
+ ids,
1084
+ nextCursor: page.nextCursor,
1085
+ done: page.nextCursor === null || page.nextCursor === void 0,
1086
+ status: "succeeded",
1087
+ loadingMore: false,
1088
+ error: null,
1089
+ counts: page.counts ?? current?.counts ?? null,
1090
+ fetchedAt: first ? Date.now() : current?.fetchedAt ?? Date.now()
1091
+ });
1092
+ } catch (e) {
1093
+ if (e instanceof AgentPinnedReadUnsupported) {
1094
+ errorSink({
1095
+ code: "picker_page_unsupported",
1096
+ message: `the agent picker fell back to the whole catalog (${e.message}); pass a supabase-js client`,
1097
+ context: { catalogId }
1098
+ });
1099
+ const current2 = state.pickerPages[key];
1100
+ writePickerPage(key, { ...current2 ?? base, status: "failed", loadingMore: false, error: "unsupported" });
1101
+ await ensureLoaded();
1102
+ return;
1103
+ }
1104
+ const message = errorMessage(e);
1105
+ const current = state.pickerPages[key];
1106
+ writePickerPage(key, { ...current ?? base, status: "failed", loadingMore: false, error: message });
1107
+ errorSink({
1108
+ code: "picker_page_failed",
1109
+ message: `the agent list could not be read: ${message}`,
1110
+ context: { catalogId, query }
1111
+ });
1112
+ throw e;
1113
+ }
1114
+ }
1115
+ function runPicker(key, run) {
1116
+ const existing = pickerInFlight.get(key);
1117
+ if (existing) return existing;
1118
+ const promise = run().finally(() => {
1119
+ if (pickerInFlight.get(key) === promise) pickerInFlight.delete(key);
1120
+ });
1121
+ pickerInFlight.set(key, promise);
1122
+ return promise;
1123
+ }
1124
+ function ensurePickerPage(query) {
1125
+ const key = agentPickerPageKey(query);
1126
+ const held = state.pickerPages[key];
1127
+ if (held && held.status === "succeeded" && Date.now() - held.fetchedAt < LIST_TTL_MS) {
1128
+ return Promise.resolve();
1129
+ }
1130
+ return runPicker(key, () => readPickerPage(query, true));
1131
+ }
1132
+ function loadMorePickerPage(query) {
1133
+ const key = agentPickerPageKey(query);
1134
+ const held = state.pickerPages[key];
1135
+ if (!held || held.done || held.status !== "succeeded" || held.loadingMore) return Promise.resolve();
1136
+ return runPicker(key, () => readPickerPage(query, false));
1137
+ }
1138
+ const rowsInFlight = /* @__PURE__ */ new Map();
1139
+ async function ensureAgentRows(agentIds) {
1140
+ if (getUserIdOrNull() === null) return;
1141
+ const waits = [];
1142
+ const wanted = [];
1143
+ for (const id of agentIds) {
1144
+ if (!id || id.startsWith("mandate:")) continue;
1145
+ const held = state.byId[id];
1146
+ if (held && !held.isThin) continue;
1147
+ const flying = rowsInFlight.get(id);
1148
+ if (flying) waits.push(flying);
1149
+ else wanted.push(id);
1150
+ }
1151
+ if (wanted.length > 0) {
1152
+ const run = (async () => {
1153
+ try {
1154
+ mergeRows(await readAgentRowsByIds(client, wanted));
1155
+ } catch (e) {
1156
+ errorSink({
1157
+ code: "agent_rows_read_failed",
1158
+ message: `agent details could not be read: ${errorMessage(e)}`,
1159
+ context: { catalogId, count: wanted.length }
1160
+ });
1161
+ throw e;
1162
+ }
1163
+ })();
1164
+ const settled = run.finally(() => {
1165
+ for (const id of wanted) if (rowsInFlight.get(id) === settled) rowsInFlight.delete(id);
1166
+ });
1167
+ for (const id of wanted) rowsInFlight.set(id, settled);
1168
+ waits.push(settled);
1169
+ }
1170
+ await Promise.all(waits);
1171
+ }
1172
+ const taggedAt = /* @__PURE__ */ new Map();
1173
+ const taggedInFlight = /* @__PURE__ */ new Map();
1174
+ function ensureTaggedRows(tags) {
1175
+ if (tags.length === 0 || isFresh() || getUserIdOrNull() === null) return Promise.resolve();
1176
+ const key = [...tags].sort().join("|");
1177
+ const at = taggedAt.get(key);
1178
+ if (at !== void 0 && Date.now() - at < LIST_TTL_MS) return Promise.resolve();
1179
+ const flying = taggedInFlight.get(key);
1180
+ if (flying) return flying;
1181
+ const run = (async () => {
1182
+ try {
1183
+ mergeRows(await readAgentTaggedRows(client, tags));
1184
+ taggedAt.set(key, Date.now());
1185
+ } catch (e) {
1186
+ if (e instanceof AgentPinnedReadUnsupported) {
1187
+ await ensureLoaded();
1188
+ return;
1189
+ }
1190
+ errorSink({
1191
+ code: "tagged_rows_read_failed",
1192
+ message: `agents tagged ${key} could not be read: ${errorMessage(e)}`,
1193
+ context: { catalogId, tags }
1194
+ });
1195
+ throw e;
1196
+ }
1197
+ })().finally(() => {
1198
+ if (taggedInFlight.get(key) === run) taggedInFlight.delete(key);
1199
+ });
1200
+ taggedInFlight.set(key, run);
1201
+ return run;
1202
+ }
980
1203
  function getConsumer(consumerId) {
981
1204
  return state.consumers[consumerId] ?? consumerDefaults;
982
1205
  }
@@ -1171,6 +1394,7 @@ function createAgentCatalog(config) {
1171
1394
  errorSink,
1172
1395
  notifier,
1173
1396
  canResolveMandates: transport !== void 0,
1397
+ pickerReads: config.pickerReads === "catalog" ? "catalog" : "page",
1174
1398
  consumerDefaults,
1175
1399
  getState: () => state,
1176
1400
  subscribe: (listener) => {
@@ -1184,6 +1408,10 @@ function createAgentCatalog(config) {
1184
1408
  ensureLoaded,
1185
1409
  ensureFavoriteRows,
1186
1410
  seedFavoriteRows,
1411
+ ensurePickerPage,
1412
+ loadMorePickerPage,
1413
+ ensureAgentRows,
1414
+ ensureTaggedRows,
1187
1415
  isFresh,
1188
1416
  isStale,
1189
1417
  getConsumer,
@@ -128,6 +128,16 @@ interface AgentSummary {
128
128
  * host can tell "the default" from a stored agent without parsing the id.
129
129
  */
130
130
  mandateKey?: string;
131
+ /**
132
+ * THE THIN ROW (FW-014). `true` when this row came from the picker's page
133
+ * read (`agent.agx_picker_page`), which carries only what a picker row
134
+ * renders: no description, model, offering, task, source or sharer. A
135
+ * consumer that needs those asks `catalog.ensureAgentRows([id])` (hover,
136
+ * open), which replaces it with the full row. Absent = a full row.
137
+ */
138
+ isThin?: boolean;
139
+ /** When the caller last used this agent (their latest conversation with it). Page reads only. */
140
+ lastUsedAt?: string | null;
131
141
  }
132
142
  type AgentSortOption = "updated-desc" | "created-desc" | "name-asc" | "name-desc" | "category-asc";
133
143
  /**
@@ -528,6 +538,15 @@ interface AgentCatalogConfig {
528
538
  transport?: AgentCatalogTransport;
529
539
  /** OPTIONAL — label overrides. */
530
540
  labels?: Partial<AgentCatalogLabels>;
541
+ /**
542
+ * How a picker reads its list (FW-014). DEFAULT `"page"`: the menu opens on
543
+ * one server page (`agent.agx_picker_page`, ~50 thin rows + counts) and pages
544
+ * on scroll; the whole catalog is read only when the person narrows by
545
+ * category/tag/sort/filter. `"catalog"`: every picker reads the whole catalog
546
+ * on open (the pre-2026-10-23 behavior) — for a host whose client cannot
547
+ * reach the `agent` schema, and for tests of the full-list rules.
548
+ */
549
+ pickerReads?: "page" | "catalog";
531
550
  /**
532
551
  * OPTIONAL — the host's starting values for per-consumer view state.
533
552
  * See `AgentCatalogDefaults`.
@@ -579,6 +598,111 @@ type AgentCatalogResolveHref = (agent: AgentSummary) => string;
579
598
  */
580
599
  type AgentCatalogOpenPeek = (agent: AgentSummary) => void;
581
600
 
601
+ /**
602
+ * catalog/data.ts — @ai-matrx/agents/catalog
603
+ *
604
+ * Every database call the catalog makes, and nothing else. Five of them:
605
+ *
606
+ * 1. `agx_get_list_full()` — the complete catalog (owned + directly-shared
607
+ * + org-shared user agents, unioned with active builtins).
608
+ * 2. `agx_search(p_query, p_deep, p_limit, p_offset)` — tier-2 server search.
609
+ * 3. `agx_resolve_agent_address(p_ids)` — the single-agent name read, used
610
+ * when a mandate's Holder is not in the caller's own catalog.
611
+ * 4. The caller's stars arrive ON the rows: every read above computes
612
+ * `is_favorite` server-side from `platform.user_entity_state`
613
+ * (`platform.my_favorite`). There is no second star read (FW-014; the
614
+ * `ues_get_bulk` overlay over every id was retired 2026-10-23).
615
+ * 5. ONE write: the caller's star, `ues_set` through the same port. The
616
+ * retired `agent.definition.is_favorite` column is never read or written.
617
+ * 6. THE PINNED READ (2026-10-09; by-ids 2026-10-23): the caller's starred
618
+ * ids (`ues_list` through the favorites port), then
619
+ * `agent.agx_get_by_ids(p_ids)` for exactly those ids. A sidebar's Pinned
620
+ * section shows a handful of rows; it never pays for the whole catalog,
621
+ * in the browser or on the server, to find them.
622
+ * 7. THE PICKER PAGE (FW-014, 2026-10-23): `agent.agx_picker_page` — the
623
+ * menu opens with ~50 thin rows (favorites, recently used, name) and the
624
+ * tab counts; scrolling reads the next keyset page. `agent.agx_get_by_ids`
625
+ * fills in a full row on hover/open. The whole-catalog read above remains
626
+ * for surfaces that genuinely list everything, and for a picker whose
627
+ * person narrows by category or tag (the facets need every row).
628
+ *
629
+ * 🚨 THE LIST READ IS COMPLETE OR IT THROWS. PostgREST caps every response at
630
+ * `db-max-rows` (1000 on Matrx Main) and reports success — matrx-frontend's
631
+ * `fetchAgentsListFull` issued a bare `.rpc()` and would silently have
632
+ * returned 1000 rows of a larger catalog (the ~400-builtin truncation
633
+ * incident is the near-miss). Here the read goes through `readAllRows` with
634
+ * `{ count: "exact" }` and a stable `.order("id")`, so a truncation is a
635
+ * loud `IncompleteReadError` instead of a confidently short picker.
636
+ * The DB's own favourites-first/`updated_at` order is irrelevant: every row
637
+ * is re-sorted client-side by `sortFilteredAgents`.
638
+ */
639
+
640
+ /** Server search page size — the frontend's `AGENT_SEARCH_LIMIT`. */
641
+ declare const AGENT_SEARCH_LIMIT = 50;
642
+ declare class AgentCatalogReadError extends Error {
643
+ readonly name = "AgentCatalogReadError";
644
+ readonly source: string;
645
+ constructor(source: string, detail: string);
646
+ }
647
+ /**
648
+ * Read the WHOLE agent catalog. Throws rather than returning a partial
649
+ * list — see the file header.
650
+ */
651
+ declare function readAgentCatalogRows(client: AgentCatalogClient): Promise<AgentSummary[]>;
652
+ interface AgentServerSearchResult {
653
+ /** Matched ids in SERVER RANK ORDER. */
654
+ ids: string[];
655
+ /** Full rows for every hit — merged additively into the registry. */
656
+ rows: AgentSummary[];
657
+ }
658
+ /**
659
+ * Tier-2 server search. Purely ADDITIVE: it widens what the local filter
660
+ * admits and never removes a row the local scorer already matched.
661
+ */
662
+ declare function searchAgentsOnServer(client: AgentCatalogClient, args: {
663
+ query: string;
664
+ deep?: boolean;
665
+ limit?: number;
666
+ }): Promise<AgentServerSearchResult>;
667
+ /**
668
+ * The single-agent read, used when a mandate's resolved Holder is not in the
669
+ * caller's own catalog (a platform agent nobody has shared, most often).
670
+ *
671
+ * Two existing doors, in order — never a new RPC:
672
+ * 1. `agx_search` with the id as the query. The scorer awards an exact id
673
+ * match 100000, so the row comes back first, and it is the FULL 19-column
674
+ * row (name AND description AND category AND tags).
675
+ * 2. `agx_resolve_agent_address([id])` — the frontend's own name resolver.
676
+ * Name and type only, so the row renders with an honest name and no
677
+ * description rather than a fabricated one.
678
+ *
679
+ * Returns `null` when neither door can name the agent. The caller SCREAMS;
680
+ * it never invents a label.
681
+ */
682
+ declare function readSingleAgentRow(client: AgentCatalogClient, agentId: string): Promise<AgentSummary | null>;
683
+ /**
684
+ * THE ONE WRITE: the caller's star on one agent, in
685
+ * `platform.user_entity_state` through the favorites port. Throws an
686
+ * `AgentCatalogReadError` carrying the refusal in words.
687
+ */
688
+ declare function writeAgentFavorite(favorites: AgentCatalogFavorites, agentId: string, isFavorite: boolean): Promise<void>;
689
+ /** Which slice of the caller's catalog a picker page reads. */
690
+ interface AgentPickerPageQuery {
691
+ /** `all_system` = the admin "All" tab that blends system agents in. */
692
+ tab: "mine" | "shared" | "all" | "all_system" | "system";
693
+ archive: "active" | "archived" | "both";
694
+ includeQuick: boolean;
695
+ }
696
+ /** The tab badges, counted server-side over the same reach as the rows. */
697
+ interface AgentPickerCounts {
698
+ mine: number;
699
+ shared: number;
700
+ all: number;
701
+ system: number;
702
+ /** Archived rows the active filter hides behind THIS tab (0 unless archive = active). */
703
+ archivedHidden: number;
704
+ }
705
+
582
706
  /**
583
707
  * catalog/default-row-types.ts — @ai-matrx/agents/catalog
584
708
  *
@@ -704,6 +828,20 @@ declare function assertAgentCatalogSchema(client: AgentCatalogClient, options?:
704
828
  */
705
829
 
706
830
  type AgentCatalogStatus = "idle" | "loading" | "succeeded" | "failed";
831
+ /** One picker slice (tab × archive × quick) read a page at a time (FW-014). */
832
+ interface AgentPickerPageState {
833
+ /** Row ids in server order across every page read so far. */
834
+ ids: string[];
835
+ /** Keyset position of the next page; `null` once the last page is in. */
836
+ nextCursor: unknown;
837
+ done: boolean;
838
+ status: AgentCatalogStatus;
839
+ /** True while a next page (not the first) is in flight. */
840
+ loadingMore: boolean;
841
+ error: string | null;
842
+ counts: AgentPickerCounts | null;
843
+ fetchedAt: number;
844
+ }
707
845
  interface AgentCatalogState {
708
846
  /** Every row, one stable array reference per change (memoization depends on it). */
709
847
  rows: AgentSummary[];
@@ -719,6 +857,8 @@ interface AgentCatalogState {
719
857
  * or by a complete catalog load, whichever came first.
720
858
  */
721
859
  favoriteRowsStatus: AgentCatalogStatus;
860
+ /** The picker's paged slices, keyed by `agentPickerPageKey` (FW-014). */
861
+ pickerPages: Record<string, AgentPickerPageState>;
722
862
  }
723
863
  interface AgentCatalog {
724
864
  readonly catalogId: string;
@@ -727,6 +867,8 @@ interface AgentCatalog {
727
867
  readonly notifier: AgentCatalogNotifier | undefined;
728
868
  /** True when this catalog was given a transport and can resolve mandates. */
729
869
  readonly canResolveMandates: boolean;
870
+ /** `AgentCatalogConfig.pickerReads`, resolved (default `"page"`). */
871
+ readonly pickerReads: "page" | "catalog";
730
872
  /**
731
873
  * The consumer state a brand-new consumer starts from, AFTER the host's
732
874
  * `defaults` knob is applied. Read it instead of
@@ -765,6 +907,28 @@ interface AgentCatalog {
765
907
  * makes no request. Rows that do not parse are skipped.
766
908
  */
767
909
  seedFavoriteRows(rawRows: unknown[]): void;
910
+ /**
911
+ * THE PICKER'S OPEN (FW-014): the first page (~50 thin rows) of one slice,
912
+ * plus its tab counts — never the whole catalog. Fresh for the list's TTL;
913
+ * deduped in flight; safe on every open.
914
+ */
915
+ ensurePickerPage(query: AgentPickerPageQuery): Promise<void>;
916
+ /** The next page of a slice (scroll). No-op when the slice is done or already loading. */
917
+ loadMorePickerPage(query: AgentPickerPageQuery): Promise<void>;
918
+ /**
919
+ * FULL rows for these ids (`agent.agx_get_by_ids`), merged into the registry:
920
+ * a hover card's details, the pinned current agent, a name for an id that is
921
+ * not on page 1. Ids already held as full rows are not asked again; deduped
922
+ * in flight.
923
+ */
924
+ ensureAgentRows(agentIds: readonly string[]): Promise<void>;
925
+ /**
926
+ * The caller's live agents carrying any of these tags, merged into the
927
+ * registry (`agent.agx_picker_tagged`) — the composer's quick agents and
928
+ * presets, never the catalog. Fresh for the list's TTL; deduped in flight;
929
+ * answered at once when the whole catalog is already fresh.
930
+ */
931
+ ensureTaggedRows(tags: readonly string[]): Promise<void>;
768
932
  /** True when the last load is inside the 15-minute TTL. */
769
933
  isFresh(): boolean;
770
934
  /** True when the last load is older than 4 hours (tab-restore threshold). */
@@ -799,89 +963,6 @@ declare function createAgentCatalog(config: AgentCatalogConfig): AgentCatalog;
799
963
  */
800
964
  declare function getRegisteredAgentCatalog(catalogId?: string): AgentCatalog | undefined;
801
965
 
802
- /**
803
- * catalog/data.ts — @ai-matrx/agents/catalog
804
- *
805
- * Every database call the catalog makes, and nothing else. Five of them:
806
- *
807
- * 1. `agx_get_list_full()` — the complete catalog (owned + directly-shared
808
- * + org-shared user agents, unioned with active builtins).
809
- * 2. `agx_search(p_query, p_deep, p_limit, p_offset)` — tier-2 server search.
810
- * 3. `agx_resolve_agent_address(p_ids)` — the single-agent name read, used
811
- * when a mandate's Holder is not in the caller's own catalog.
812
- * 4. The caller's stars arrive ON the rows: every read above computes
813
- * `is_favorite` server-side from `platform.user_entity_state`
814
- * (`platform.my_favorite`). There is no second star read (FW-014; the
815
- * `ues_get_bulk` overlay over every id was retired 2026-10-23).
816
- * 5. ONE write: the caller's star, `ues_set` through the same port. The
817
- * retired `agent.definition.is_favorite` column is never read or written.
818
- * 6. THE PINNED READ (2026-10-09; by-ids 2026-10-23): the caller's starred
819
- * ids (`ues_list` through the favorites port), then
820
- * `agent.agx_get_by_ids(p_ids)` for exactly those ids. A sidebar's Pinned
821
- * section shows a handful of rows; it never pays for the whole catalog,
822
- * in the browser or on the server, to find them.
823
- *
824
- * 🚨 THE LIST READ IS COMPLETE OR IT THROWS. PostgREST caps every response at
825
- * `db-max-rows` (1000 on Matrx Main) and reports success — matrx-frontend's
826
- * `fetchAgentsListFull` issued a bare `.rpc()` and would silently have
827
- * returned 1000 rows of a larger catalog (the ~400-builtin truncation
828
- * incident is the near-miss). Here the read goes through `readAllRows` with
829
- * `{ count: "exact" }` and a stable `.order("id")`, so a truncation is a
830
- * loud `IncompleteReadError` instead of a confidently short picker.
831
- * The DB's own favourites-first/`updated_at` order is irrelevant: every row
832
- * is re-sorted client-side by `sortFilteredAgents`.
833
- */
834
-
835
- /** Server search page size — the frontend's `AGENT_SEARCH_LIMIT`. */
836
- declare const AGENT_SEARCH_LIMIT = 50;
837
- declare class AgentCatalogReadError extends Error {
838
- readonly name = "AgentCatalogReadError";
839
- readonly source: string;
840
- constructor(source: string, detail: string);
841
- }
842
- /**
843
- * Read the WHOLE agent catalog. Throws rather than returning a partial
844
- * list — see the file header.
845
- */
846
- declare function readAgentCatalogRows(client: AgentCatalogClient): Promise<AgentSummary[]>;
847
- interface AgentServerSearchResult {
848
- /** Matched ids in SERVER RANK ORDER. */
849
- ids: string[];
850
- /** Full rows for every hit — merged additively into the registry. */
851
- rows: AgentSummary[];
852
- }
853
- /**
854
- * Tier-2 server search. Purely ADDITIVE: it widens what the local filter
855
- * admits and never removes a row the local scorer already matched.
856
- */
857
- declare function searchAgentsOnServer(client: AgentCatalogClient, args: {
858
- query: string;
859
- deep?: boolean;
860
- limit?: number;
861
- }): Promise<AgentServerSearchResult>;
862
- /**
863
- * The single-agent read, used when a mandate's resolved Holder is not in the
864
- * caller's own catalog (a platform agent nobody has shared, most often).
865
- *
866
- * Two existing doors, in order — never a new RPC:
867
- * 1. `agx_search` with the id as the query. The scorer awards an exact id
868
- * match 100000, so the row comes back first, and it is the FULL 19-column
869
- * row (name AND description AND category AND tags).
870
- * 2. `agx_resolve_agent_address([id])` — the frontend's own name resolver.
871
- * Name and type only, so the row renders with an honest name and no
872
- * description rather than a fabricated one.
873
- *
874
- * Returns `null` when neither door can name the agent. The caller SCREAMS;
875
- * it never invents a label.
876
- */
877
- declare function readSingleAgentRow(client: AgentCatalogClient, agentId: string): Promise<AgentSummary | null>;
878
- /**
879
- * THE ONE WRITE: the caller's star on one agent, in
880
- * `platform.user_entity_state` through the favorites port. Throws an
881
- * `AgentCatalogReadError` carrying the refusal in words.
882
- */
883
- declare function writeAgentFavorite(favorites: AgentCatalogFavorites, agentId: string, isFavorite: boolean): Promise<void>;
884
-
885
966
  /**
886
967
  * catalog/rows.ts — @ai-matrx/agents/catalog
887
968
  *