@ai-matrx/agents 0.59.264 → 0.59.265

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.
@@ -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
  *
@@ -150,7 +150,8 @@ function toAgentSummary(raw) {
150
150
  orchestra: orchestraBadge(raw["orchestra"]),
151
151
  // Present only when the read carries the column: absent ≠ "no pin".
152
152
  ..."offering_id" in raw ? { offeringId: str(raw["offering_id"]) } : {},
153
- ..."organization_name" in raw ? { organizationName: str(raw["organization_name"]) } : {}
153
+ ..."organization_name" in raw ? { organizationName: str(raw["organization_name"]) } : {},
154
+ ..."last_used_at" in raw ? { lastUsedAt: str(raw["last_used_at"]) } : {}
154
155
  };
155
156
  }
156
157
 
@@ -289,20 +290,77 @@ async function readFavoriteAgentRows(client, favorites) {
289
290
  )
290
291
  );
291
292
  if (ids.length === 0) return [];
293
+ const rows = await readAgentRowsByIds(client, ids, "agent.agx_get_by_ids (pinned)");
294
+ return rows.map((summary) => summary.isFavorite ? summary : { ...summary, isFavorite: true });
295
+ }
296
+ async function readAgentRowsByIds(client, agentIds, label = "agent.agx_get_by_ids") {
297
+ if (typeof client.schema !== "function") {
298
+ throw new AgentPinnedReadUnsupported("the client has no schema()");
299
+ }
300
+ const ids = Array.from(new Set(agentIds.filter((id) => typeof id === "string" && id.length > 0)));
292
301
  const rows = [];
293
302
  for (let at = 0; at < ids.length; at += PINNED_ID_CHUNK) {
294
303
  const chunk = ids.slice(at, at + PINNED_ID_CHUNK);
295
304
  const { data, error } = await client.schema("agent").rpc("agx_get_by_ids", { p_ids: chunk });
296
305
  if (error) {
297
- throw new AgentCatalogReadError("agent.agx_get_by_ids (pinned)", pgDetail(error));
306
+ throw new AgentCatalogReadError(label, pgDetail(error));
298
307
  }
299
308
  for (const item of Array.isArray(data) ? data : []) {
300
309
  const summary = toAgentSummary(item);
301
- if (summary) rows.push(summary.isFavorite ? summary : { ...summary, isFavorite: true });
310
+ if (summary) rows.push(summary);
302
311
  }
303
312
  }
304
313
  return rows;
305
314
  }
315
+ var PICKER_PAGE_SIZE = 50;
316
+ function countOf(record, key) {
317
+ const value = record[key];
318
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
319
+ }
320
+ async function readAgentPickerPage(client, query, cursor = null, limit = PICKER_PAGE_SIZE) {
321
+ if (typeof client.schema !== "function") {
322
+ throw new AgentPinnedReadUnsupported("the client has no schema()");
323
+ }
324
+ const { data, error } = await client.schema("agent").rpc("agx_picker_page", {
325
+ p_tab: query.tab,
326
+ p_archive: query.archive,
327
+ p_include_quick: query.includeQuick,
328
+ p_cursor: cursor ?? null,
329
+ p_limit: limit
330
+ });
331
+ if (error) throw new AgentCatalogReadError("agent.agx_picker_page", pgDetail(error));
332
+ const record = typeof data === "object" && data !== null && !Array.isArray(data) ? data : null;
333
+ if (!record) {
334
+ throw new AgentCatalogReadError("agent.agx_picker_page", "the page came back without a body");
335
+ }
336
+ const rows = [];
337
+ for (const item of Array.isArray(record["rows"]) ? record["rows"] : []) {
338
+ const summary = toAgentSummary(item);
339
+ if (summary) rows.push({ ...summary, isThin: true });
340
+ }
341
+ const rawCounts = record["counts"];
342
+ const counts = typeof rawCounts === "object" && rawCounts !== null && !Array.isArray(rawCounts) ? {
343
+ mine: countOf(rawCounts, "mine"),
344
+ shared: countOf(rawCounts, "shared"),
345
+ all: countOf(rawCounts, "all"),
346
+ system: countOf(rawCounts, "system"),
347
+ archivedHidden: countOf(rawCounts, "archived_hidden")
348
+ } : null;
349
+ return { rows, nextCursor: record["next_cursor"] ?? null, counts };
350
+ }
351
+ async function readAgentTaggedRows(client, tags) {
352
+ if (typeof client.schema !== "function") {
353
+ throw new AgentPinnedReadUnsupported("the client has no schema()");
354
+ }
355
+ const { data, error } = await client.schema("agent").rpc("agx_picker_tagged", { p_tags: [...tags] });
356
+ if (error) throw new AgentCatalogReadError("agent.agx_picker_tagged", pgDetail(error));
357
+ const rows = [];
358
+ for (const item of Array.isArray(data) ? data : []) {
359
+ const summary = toAgentSummary(item);
360
+ if (summary) rows.push({ ...summary, isThin: true });
361
+ }
362
+ return rows;
363
+ }
306
364
 
307
365
  // catalog/default-row.ts
308
366
  var CACHE_PREFIX = "ai-matrx.agents.catalog.default-row:";
@@ -640,6 +698,9 @@ function readsOrchestraColumn(data) {
640
698
  // catalog/store.ts
641
699
  var LIST_TTL_MS = 15 * 60 * 1e3;
642
700
  var LIST_STALE_MS = 4 * 60 * 60 * 1e3;
701
+ function agentPickerPageKey(query) {
702
+ return `${query.tab}|${query.archive}|${query.includeQuick ? "q" : "-"}`;
703
+ }
643
704
  function createDefaultErrorSink() {
644
705
  let announced = false;
645
706
  return (event) => {
@@ -723,7 +784,8 @@ function createAgentCatalog(config) {
723
784
  error: null,
724
785
  consumers: {},
725
786
  defaultRows: {},
726
- favoriteRowsStatus: "idle"
787
+ favoriteRowsStatus: "idle",
788
+ pickerPages: {}
727
789
  };
728
790
  const listeners = /* @__PURE__ */ new Set();
729
791
  function publish(next) {
@@ -733,7 +795,10 @@ function createAgentCatalog(config) {
733
795
  function mergeRows(incoming) {
734
796
  if (incoming.length === 0) return;
735
797
  const byId = { ...state.byId };
736
- for (const row of incoming) byId[row.id] = row;
798
+ for (const row of incoming) {
799
+ const held = byId[row.id];
800
+ byId[row.id] = row.isThin && held && !held.isThin ? { ...held, isFavorite: row.isFavorite, ...row.lastUsedAt !== void 0 ? { lastUsedAt: row.lastUsedAt } : {} } : row;
801
+ }
737
802
  publish({ ...state, byId, rows: Object.values(byId) });
738
803
  }
739
804
  function replaceRows(incoming) {
@@ -871,6 +936,164 @@ function createAgentCatalog(config) {
871
936
  favoriteRowsInFlight = run;
872
937
  return run;
873
938
  }
939
+ const pickerInFlight = /* @__PURE__ */ new Map();
940
+ function writePickerPage(key, next) {
941
+ publish({ ...state, pickerPages: { ...state.pickerPages, [key]: next } });
942
+ }
943
+ async function readPickerPage(query, first) {
944
+ const key = agentPickerPageKey(query);
945
+ const prior = state.pickerPages[key];
946
+ if (getUserIdOrNull() === null) {
947
+ 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() });
948
+ return;
949
+ }
950
+ const base = {
951
+ ids: [],
952
+ nextCursor: null,
953
+ done: false,
954
+ status: "loading",
955
+ loadingMore: false,
956
+ error: null,
957
+ counts: null,
958
+ fetchedAt: 0
959
+ };
960
+ if (first) {
961
+ if (!prior || prior.status === "failed") writePickerPage(key, { ...prior ?? base, status: "loading", error: null });
962
+ } else if (prior) {
963
+ writePickerPage(key, { ...prior, loadingMore: true, error: null });
964
+ }
965
+ try {
966
+ const page = await readAgentPickerPage(client, query, first ? null : prior?.nextCursor ?? null);
967
+ mergeRows(page.rows);
968
+ const current = state.pickerPages[key];
969
+ const keptIds = first ? [] : current?.ids ?? [];
970
+ const seen = new Set(keptIds);
971
+ const ids = [...keptIds];
972
+ for (const row of page.rows) if (!seen.has(row.id)) {
973
+ seen.add(row.id);
974
+ ids.push(row.id);
975
+ }
976
+ writePickerPage(key, {
977
+ ids,
978
+ nextCursor: page.nextCursor,
979
+ done: page.nextCursor === null || page.nextCursor === void 0,
980
+ status: "succeeded",
981
+ loadingMore: false,
982
+ error: null,
983
+ counts: page.counts ?? current?.counts ?? null,
984
+ fetchedAt: first ? Date.now() : current?.fetchedAt ?? Date.now()
985
+ });
986
+ } catch (e) {
987
+ if (e instanceof AgentPinnedReadUnsupported) {
988
+ errorSink({
989
+ code: "picker_page_unsupported",
990
+ message: `the agent picker fell back to the whole catalog (${e.message}); pass a supabase-js client`,
991
+ context: { catalogId }
992
+ });
993
+ const current2 = state.pickerPages[key];
994
+ writePickerPage(key, { ...current2 ?? base, status: "failed", loadingMore: false, error: "unsupported" });
995
+ await ensureLoaded();
996
+ return;
997
+ }
998
+ const message = errorMessage(e);
999
+ const current = state.pickerPages[key];
1000
+ writePickerPage(key, { ...current ?? base, status: "failed", loadingMore: false, error: message });
1001
+ errorSink({
1002
+ code: "picker_page_failed",
1003
+ message: `the agent list could not be read: ${message}`,
1004
+ context: { catalogId, query }
1005
+ });
1006
+ throw e;
1007
+ }
1008
+ }
1009
+ function runPicker(key, run) {
1010
+ const existing = pickerInFlight.get(key);
1011
+ if (existing) return existing;
1012
+ const promise = run().finally(() => {
1013
+ if (pickerInFlight.get(key) === promise) pickerInFlight.delete(key);
1014
+ });
1015
+ pickerInFlight.set(key, promise);
1016
+ return promise;
1017
+ }
1018
+ function ensurePickerPage(query) {
1019
+ const key = agentPickerPageKey(query);
1020
+ const held = state.pickerPages[key];
1021
+ if (held && held.status === "succeeded" && Date.now() - held.fetchedAt < LIST_TTL_MS) {
1022
+ return Promise.resolve();
1023
+ }
1024
+ return runPicker(key, () => readPickerPage(query, true));
1025
+ }
1026
+ function loadMorePickerPage(query) {
1027
+ const key = agentPickerPageKey(query);
1028
+ const held = state.pickerPages[key];
1029
+ if (!held || held.done || held.status !== "succeeded" || held.loadingMore) return Promise.resolve();
1030
+ return runPicker(key, () => readPickerPage(query, false));
1031
+ }
1032
+ const rowsInFlight = /* @__PURE__ */ new Map();
1033
+ async function ensureAgentRows(agentIds) {
1034
+ if (getUserIdOrNull() === null) return;
1035
+ const waits = [];
1036
+ const wanted = [];
1037
+ for (const id of agentIds) {
1038
+ if (!id || id.startsWith("mandate:")) continue;
1039
+ const held = state.byId[id];
1040
+ if (held && !held.isThin) continue;
1041
+ const flying = rowsInFlight.get(id);
1042
+ if (flying) waits.push(flying);
1043
+ else wanted.push(id);
1044
+ }
1045
+ if (wanted.length > 0) {
1046
+ const run = (async () => {
1047
+ try {
1048
+ mergeRows(await readAgentRowsByIds(client, wanted));
1049
+ } catch (e) {
1050
+ errorSink({
1051
+ code: "agent_rows_read_failed",
1052
+ message: `agent details could not be read: ${errorMessage(e)}`,
1053
+ context: { catalogId, count: wanted.length }
1054
+ });
1055
+ throw e;
1056
+ }
1057
+ })();
1058
+ const settled = run.finally(() => {
1059
+ for (const id of wanted) if (rowsInFlight.get(id) === settled) rowsInFlight.delete(id);
1060
+ });
1061
+ for (const id of wanted) rowsInFlight.set(id, settled);
1062
+ waits.push(settled);
1063
+ }
1064
+ await Promise.all(waits);
1065
+ }
1066
+ const taggedAt = /* @__PURE__ */ new Map();
1067
+ const taggedInFlight = /* @__PURE__ */ new Map();
1068
+ function ensureTaggedRows(tags) {
1069
+ if (tags.length === 0 || isFresh() || getUserIdOrNull() === null) return Promise.resolve();
1070
+ const key = [...tags].sort().join("|");
1071
+ const at = taggedAt.get(key);
1072
+ if (at !== void 0 && Date.now() - at < LIST_TTL_MS) return Promise.resolve();
1073
+ const flying = taggedInFlight.get(key);
1074
+ if (flying) return flying;
1075
+ const run = (async () => {
1076
+ try {
1077
+ mergeRows(await readAgentTaggedRows(client, tags));
1078
+ taggedAt.set(key, Date.now());
1079
+ } catch (e) {
1080
+ if (e instanceof AgentPinnedReadUnsupported) {
1081
+ await ensureLoaded();
1082
+ return;
1083
+ }
1084
+ errorSink({
1085
+ code: "tagged_rows_read_failed",
1086
+ message: `agents tagged ${key} could not be read: ${errorMessage(e)}`,
1087
+ context: { catalogId, tags }
1088
+ });
1089
+ throw e;
1090
+ }
1091
+ })().finally(() => {
1092
+ if (taggedInFlight.get(key) === run) taggedInFlight.delete(key);
1093
+ });
1094
+ taggedInFlight.set(key, run);
1095
+ return run;
1096
+ }
874
1097
  function getConsumer(consumerId) {
875
1098
  return state.consumers[consumerId] ?? consumerDefaults;
876
1099
  }
@@ -1065,6 +1288,7 @@ function createAgentCatalog(config) {
1065
1288
  errorSink,
1066
1289
  notifier,
1067
1290
  canResolveMandates: transport !== void 0,
1291
+ pickerReads: config.pickerReads === "catalog" ? "catalog" : "page",
1068
1292
  consumerDefaults,
1069
1293
  getState: () => state,
1070
1294
  subscribe: (listener) => {
@@ -1078,6 +1302,10 @@ function createAgentCatalog(config) {
1078
1302
  ensureLoaded,
1079
1303
  ensureFavoriteRows,
1080
1304
  seedFavoriteRows,
1305
+ ensurePickerPage,
1306
+ loadMorePickerPage,
1307
+ ensureAgentRows,
1308
+ ensureTaggedRows,
1081
1309
  isFresh,
1082
1310
  isStale,
1083
1311
  getConsumer,