@ai-matrx/agents 0.9.0 → 0.9.1

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,5 +1,113 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1 — 2026-09-08
4
+
5
+ **TWO SCREENS TOLD THE SAME USER TWO DIFFERENT TRUTHS. BOTH ARE FIXED HERE, IN
6
+ THE PACKAGE, AT THE CLASS.**
7
+
8
+ Independent final verification of the picker unification (2026-09-08, same
9
+ account signed in on both surfaces) found the tab strip, the counts, the
10
+ Mine/Shared/All order, the five sort options, the tri-state favourites control,
11
+ the 42 categories, the 871 tags and four tag-only searches identical between
12
+ `aimatrx.com/chat/new` and `workflows.aimatrx.com`. Two things were not.
13
+
14
+ ### Consumer action: none
15
+
16
+ No export was removed, renamed or re-signed. Three symbols were ADDED —
17
+ `compareAgentIds`, `agentMatchesArchiveFilter`, `selectAgentTabCounts` — and the
18
+ web `AgentRow` gained a `data-testid` matching the native row's `testID`.
19
+ Every host adopts this by taking `latest`; there is nothing to change.
20
+
21
+ ### Behavioural deltas (both deliberate, both were defects)
22
+
23
+ **F1 — every sort is now a TOTAL order, ending on `id` ASC.**
24
+
25
+ The Public tab diverged at index 10. 17 rows tied on
26
+ `updated_at = 2026-09-08T06:52:46.430326+00:00`, and the two hosts fetch the
27
+ same RPC through different transports:
28
+
29
+ | host | request | order of tied rows |
30
+ |---|---|---|
31
+ | matrx-frontend | `POST /rpc/agx_get_list_full` (one shot) | the RPC's own |
32
+ | workflow-studio | `…?order=id.asc&offset=0&limit=1000` (`readAllRows`) | id ASC |
33
+
34
+ Neither client was wrong about the ROWS. The sort was not total: each
35
+ comparator branch returned `0` for tied rows, so `Array.prototype.sort` left
36
+ them wherever the transport had put them. `sortFilteredAgents` and
37
+ `applyAgentSortComparator` now end on the same final key the database itself
38
+ ends on — `public.agx_get_list` is
39
+ `ORDER BY is_favorite DESC, updated_at DESC, id` — giving:
40
+
41
+ searching: score DESC -> server rank ASC -> sort key -> id ASC
42
+ not searching: favourites first (when asked) -> sort key -> id ASC
43
+
44
+ Comparison is a codepoint `<`/`>`, never `localeCompare`: Postgres orders
45
+ `uuid` by its raw bytes and a collator is free to fold case or ignore the
46
+ dashes, which would diverge from the server on exactly these ties. A sort key
47
+ that yields `NaN` (an unparseable timestamp) now collapses to the id key
48
+ instead of defeating it.
49
+
50
+ *What the frontend used to do on ties: whatever order the RPC page came back
51
+ in.* That was never a decision — it was the absence of one, and it happened to
52
+ look stable only until a second host fetched the same rows a different way. The
53
+ order is now DEFINED, is the server's own, and is a function of the rows alone.
54
+
55
+ **F5 — a tab badge counts exactly what the tab renders.**
56
+
57
+ The Mine badge said 416 while the list rendered 412 rows and the footer said
58
+ "412 agents": 4 owned and 2 shared ARCHIVED agents were counted by the badge
59
+ and (correctly) hidden by the list. One screen, three numbers, one of them
60
+ untrue about agents the user cannot see or reach from there. This is "a screen
61
+ never lies", so it is a fix and not a knob.
62
+
63
+ `selectAgentTabCounts(rows, archFilter)` is the new consumer-aware badge
64
+ source used by `useAgentListCore`, and the four plain aggregate counts
65
+ (`selectTotalUserAgentsCount`, `…Owned…`, `…Shared…`, `…Favorite…`) now honour
66
+ the same archive rule the list uses. Flip the archive filter and the badges
67
+ follow the list into the archive. `selectTotalBuiltinAgentsCount` is
68
+ deliberately unchanged: `filterBuiltinTypeAgents` applies no archive filter
69
+ (a builtin belongs to the platform and is never the user's to archive), so it
70
+ already counted exactly what it rendered.
71
+
72
+ ### The proof
73
+
74
+ `catalog/__tests__/parity.test.ts`'s 66-row fixture had NO ties, which is why
75
+ 293 green cases never saw F1. The new fixture does:
76
+ `catalog/__tests__/total-order-fixture.json` — 42 rows, every one on that exact
77
+ live `updated_at`, 26 of them also tied on `created_at`, with name and category
78
+ tie groups on top, ids deliberately uncorrelated with emission order.
79
+
80
+ `total-order-expected.json` is generated **from the RULE** by
81
+ `scripts/generate-total-order-fixture.mjs` — favourites -> key -> id ASC,
82
+ written out independently and importing nothing from `catalog/selectors.ts`.
83
+ Regenerating it by running the shipped comparator would have made the test a
84
+ mirror of the code; it measures the specification instead.
85
+
86
+ 720 cases (tab x sort x favouritesFirst x archive x favourite x access) are
87
+ asserted three times over — kernel (`catalog/__tests__/total-order.test.ts`),
88
+ web shell (`catalog/react/total-order.test.tsx`) and native shell
89
+ (`catalog/native/__tests__/total-order.test.tsx`) — each reading the RENDERED
90
+ row ids, not the selector's return value.
91
+
92
+ Falsifiability, on every leg:
93
+
94
+ - the same rows are fed in four different input orders (identity, two seeded
95
+ shuffles, reversed) and the answer must be byte-identical — this is the live
96
+ defect, reproduced as a test;
97
+ - both shells mount the picker twice with the row array reversed and compare
98
+ screen to screen;
99
+ - deleting the id tiebreaker turns 4 of the 10 kernel tests red (verified
100
+ before shipping), and a key-only sort of the same fixture is asserted to
101
+ disagree with itself under reversal;
102
+ - the F5 legs assert the OLD counting rule produces a DIFFERENT number on this
103
+ fixture, so "badge == rows" cannot pass vacuously.
104
+
105
+ `parity.test.ts`'s count assertion now states the F5 delta explicitly — the
106
+ recorded matrx-frontend number minus the archived rows the list hides — rather
107
+ than reproducing a screen that lies. Its 293-case ordering matrix and the
108
+ native shell's 293-case matrix are UNCHANGED and still green: the old fixture
109
+ had no ties, so F1 moved nothing that was already defined.
110
+
3
111
  ## 0.9.0 — 2026-09-08
4
112
 
5
113
  **THE ONE PICKER NOW RENDERS ON REACT NATIVE — `@ai-matrx/agents/catalog/native`.**
@@ -34,10 +34,12 @@ __export(catalog_exports, {
34
34
  _resetCatalogGlobalState: () => _resetCatalogGlobalState,
35
35
  agentConsumerHasActiveFilters: () => agentConsumerHasActiveFilters,
36
36
  agentListEmptyLabel: () => agentListEmptyLabel,
37
+ agentMatchesArchiveFilter: () => agentMatchesArchiveFilter,
37
38
  agentMatchesSearch: () => agentMatchesSearch,
38
39
  applyAgentSortComparator: () => applyAgentSortComparator,
39
40
  asAgentId: () => asAgentId,
40
41
  assertAgentCatalogSchema: () => assertAgentCatalogSchema,
42
+ compareAgentIds: () => compareAgentIds,
41
43
  computeAgentSearchScore: () => computeAgentSearchScore,
42
44
  createAgentCatalog: () => createAgentCatalog,
43
45
  createMemoryStorage: () => createMemoryStorage,
@@ -73,6 +75,7 @@ __export(catalog_exports, {
73
75
  readSingleAgentRow: () => readSingleAgentRow,
74
76
  resolveDefaultRow: () => resolveDefaultRow,
75
77
  searchAgentsOnServer: () => searchAgentsOnServer,
78
+ selectAgentTabCounts: () => selectAgentTabCounts,
76
79
  selectAllAgentCategories: () => selectAllAgentCategories,
77
80
  selectAllAgentTags: () => selectAllAgentTags,
78
81
  selectAllSystemAgentCategories: () => selectAllSystemAgentCategories,
@@ -314,8 +317,8 @@ async function readSingleAgentRow(client, agentId) {
314
317
  }
315
318
  return null;
316
319
  }
317
- async function writeAgentFavorite(client, agentId, isFavorite) {
318
- const { error } = await client.schema("agent").from("definition").update({ is_favorite: isFavorite }).eq("id", agentId);
320
+ async function writeAgentFavorite(client, agentId, isFavorite2) {
321
+ const { error } = await client.schema("agent").from("definition").update({ is_favorite: isFavorite2 }).eq("id", agentId);
319
322
  if (error) {
320
323
  throw new AgentCatalogReadError(
321
324
  "agent.definition.is_favorite",
@@ -893,7 +896,7 @@ function createAgentCatalog(config) {
893
896
  mergeRows(result.rows);
894
897
  return result.ids;
895
898
  }
896
- async function setFavorite(agentId, isFavorite) {
899
+ async function setFavorite(agentId, isFavorite2) {
897
900
  const existing = state.byId[agentId];
898
901
  if (!existing) {
899
902
  errorSink({
@@ -902,15 +905,15 @@ function createAgentCatalog(config) {
902
905
  });
903
906
  return;
904
907
  }
905
- mergeRows([{ ...existing, isFavorite }]);
908
+ mergeRows([{ ...existing, isFavorite: isFavorite2 }]);
906
909
  try {
907
- await writeAgentFavorite(client, agentId, isFavorite);
910
+ await writeAgentFavorite(client, agentId, isFavorite2);
908
911
  } catch (e) {
909
912
  mergeRows([existing]);
910
913
  errorSink({
911
914
  code: "favorite_write_failed",
912
915
  message: `the favorite could not be saved: ${errorMessage(e)}`,
913
- context: { agentId, isFavorite }
916
+ context: { agentId, isFavorite: isFavorite2 }
914
917
  });
915
918
  notifier?.({
916
919
  level: "error",
@@ -1189,7 +1192,19 @@ function memoize3(fn) {
1189
1192
  return last;
1190
1193
  };
1191
1194
  }
1195
+ function compareAgentIds(a, b) {
1196
+ const idA = a.id ?? "";
1197
+ const idB = b.id ?? "";
1198
+ if (idA < idB) return -1;
1199
+ if (idA > idB) return 1;
1200
+ return 0;
1201
+ }
1192
1202
  function applyAgentSortComparator(a, b, sortBy) {
1203
+ const primary = compareAgentSortKey(a, b, sortBy);
1204
+ if (!Number.isFinite(primary) || primary === 0) return compareAgentIds(a, b);
1205
+ return primary;
1206
+ }
1207
+ function compareAgentSortKey(a, b, sortBy) {
1193
1208
  switch (sortBy) {
1194
1209
  case "name-asc":
1195
1210
  return (a.name ?? "").localeCompare(b.name ?? "");
@@ -1437,21 +1452,45 @@ var makeSelectFilteredBuiltinAgentsCount = () => {
1437
1452
  function agentConsumerHasActiveFilters(consumer, defaults) {
1438
1453
  return consumer.tab !== defaults.tab || consumer.sortBy !== defaults.sortBy || consumer.searchTerm !== "" || consumer.includedCats.length > 0 || consumer.includedTags.length > 0 || consumer.favFilter !== defaults.favFilter || consumer.archFilter !== defaults.archFilter || consumer.accessFilter !== defaults.accessFilter || consumer.favoritesFirst !== defaults.favoritesFirst;
1439
1454
  }
1455
+ function agentMatchesArchiveFilter(agent, archFilter) {
1456
+ if (archFilter === "active") return !agent.isArchived;
1457
+ if (archFilter === "archived") return Boolean(agent.isArchived);
1458
+ return true;
1459
+ }
1460
+ function countUserRows(agents, archFilter, predicate) {
1461
+ let n = 0;
1462
+ for (const a of agents) {
1463
+ if (a.agentType !== "user") continue;
1464
+ if (!agentMatchesArchiveFilter(a, archFilter)) continue;
1465
+ if (predicate(a)) n += 1;
1466
+ }
1467
+ return n;
1468
+ }
1469
+ var isOwned = (a) => a.isOwner === true;
1470
+ var isShared = (a) => a.isOwner === false && a.accessLevel != null;
1471
+ var isAny = () => true;
1472
+ var isFavorite = (a) => Boolean(a.isFavorite);
1473
+ var selectAgentTabCounts = memoize2(
1474
+ (agents, archFilter) => ({
1475
+ mine: countUserRows(agents, archFilter, isOwned),
1476
+ shared: countUserRows(agents, archFilter, isShared),
1477
+ all: countUserRows(agents, archFilter, isAny),
1478
+ system: selectBuiltinTypeAgents(agents).length
1479
+ })
1480
+ );
1440
1481
  var selectTotalUserAgentsCount = memoize1(
1441
- (agents) => selectUserTypeAgents(agents).length
1482
+ (agents) => countUserRows(agents, "active", isAny)
1442
1483
  );
1443
1484
  var selectTotalOwnedAgentsCount = memoize1(
1444
- (agents) => selectUserTypeAgents(agents).filter((a) => a.isOwner === true).length
1485
+ (agents) => countUserRows(agents, "active", isOwned)
1445
1486
  );
1446
1487
  var selectTotalSharedAgentsCount = memoize1(
1447
- (agents) => selectUserTypeAgents(agents).filter(
1448
- (a) => a.isOwner === false && a.accessLevel != null
1449
- ).length
1488
+ (agents) => countUserRows(agents, "active", isShared)
1450
1489
  );
1451
1490
  var selectTotalBuiltinAgentsCount = memoize1(
1452
1491
  (agents) => selectBuiltinTypeAgents(agents).length
1453
1492
  );
1454
1493
  var selectTotalFavoriteAgentsCount = memoize1(
1455
- (agents) => selectUserTypeAgents(agents).filter((a) => a.isFavorite).length
1494
+ (agents) => countUserRows(agents, "active", isFavorite)
1456
1495
  );
1457
1496
  //# sourceMappingURL=index.cjs.map