@ai-matrx/agents 0.59.215 → 0.59.217

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,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.59.215
3
+ ## 0.59.217
4
4
 
5
5
  - **Glyph path data removed; the same exports now alias the one glyph home (one-canonical, glyph lane, 2026-10-21).** `context, models, settings and versions icon files` defines no SVG any more: each export is `@ai-matrx/design-system/icons`'s glyph with this package's historical size and stroke. Names, props and sizes are unchanged; catalog `FolderIcon`, `TagIcon` and `RotateCcwIcon` are re-exports of the glyph home, and the settings rows join classes with `cn` from `@ai-matrx/design-system/cn`. Consumer action: none.
6
6
 
@@ -160,7 +160,11 @@ function resolveContextRule(input) {
160
160
  include = false;
161
161
  includeBy = "agent";
162
162
  }
163
- if (user && typeof user.include === "boolean") {
163
+ const ownLocked = !identity && surface?.own === true;
164
+ if (ownLocked) {
165
+ include = false;
166
+ includeBy = "page";
167
+ } else if (user && typeof user.include === "boolean") {
164
168
  include = user.include;
165
169
  includeBy = "you";
166
170
  }
@@ -113,6 +113,12 @@ interface ContextSurfaceLayer {
113
113
  auto_context?: boolean | null;
114
114
  /** `inlineUpTo` / envelope `max_inline_chars` / DB column. */
115
115
  max_inline_chars?: number | null;
116
+ /**
117
+ * The value is withheld because this conversation IS the page (`page_context` mode `own`):
118
+ * nothing re-opens it — not the agent's Context Policy, not the person's rule. A page's own
119
+ * chat never receives itself (Arman, 2026-10-01 / 2026-10-21).
120
+ */
121
+ own?: boolean;
116
122
  }
117
123
  /** The non-person layers for one value — what the table's layer breakdown shows. */
118
124
  interface ContextRuleLayers {
@@ -113,6 +113,12 @@ interface ContextSurfaceLayer {
113
113
  auto_context?: boolean | null;
114
114
  /** `inlineUpTo` / envelope `max_inline_chars` / DB column. */
115
115
  max_inline_chars?: number | null;
116
+ /**
117
+ * The value is withheld because this conversation IS the page (`page_context` mode `own`):
118
+ * nothing re-opens it — not the agent's Context Policy, not the person's rule. A page's own
119
+ * chat never receives itself (Arman, 2026-10-01 / 2026-10-21).
120
+ */
121
+ own?: boolean;
116
122
  }
117
123
  /** The non-person layers for one value — what the table's layer breakdown shows. */
118
124
  interface ContextRuleLayers {
@@ -97,7 +97,11 @@ function resolveContextRule(input) {
97
97
  include = false;
98
98
  includeBy = "agent";
99
99
  }
100
- if (user && typeof user.include === "boolean") {
100
+ const ownLocked = !identity && surface?.own === true;
101
+ if (ownLocked) {
102
+ include = false;
103
+ includeBy = "page";
104
+ } else if (user && typeof user.include === "boolean") {
101
105
  include = user.include;
102
106
  includeBy = "you";
103
107
  }
@@ -103,7 +103,11 @@ function resolveContextRule(input) {
103
103
  include = false;
104
104
  includeBy = "agent";
105
105
  }
106
- if (user && typeof user.include === "boolean") {
106
+ const ownLocked = !identity && surface?.own === true;
107
+ if (ownLocked) {
108
+ include = false;
109
+ includeBy = "page";
110
+ } else if (user && typeof user.include === "boolean") {
107
111
  include = user.include;
108
112
  includeBy = "you";
109
113
  }
@@ -218,6 +222,7 @@ function includeChanges(rows, include, cap) {
218
222
  const changes = [];
219
223
  for (const row of rows) {
220
224
  if (row.include === include) continue;
225
+ if (row.layers?.surface?.own === true) continue;
221
226
  const next = { ...row.userRule ?? {} };
222
227
  if (inheritedInclude(row, cap) === include) delete next.include;
223
228
  else next.include = include;
@@ -763,6 +768,7 @@ function ContextRuleRow({
763
768
  size: "sm",
764
769
  "aria-label": `Include ${row.label}`,
765
770
  checked: row.include,
771
+ disabled: row.layers?.surface?.own === true,
766
772
  onCheckedChange: (next) => onChange(row.key, row.surfaceKey, withInclude(row, next))
767
773
  }
768
774
  ) }),
@@ -31,6 +31,12 @@ interface ContextSurfaceLayer {
31
31
  auto_context?: boolean | null;
32
32
  /** `inlineUpTo` / envelope `max_inline_chars` / DB column. */
33
33
  max_inline_chars?: number | null;
34
+ /**
35
+ * The value is withheld because this conversation IS the page (`page_context` mode `own`):
36
+ * nothing re-opens it — not the agent's Context Policy, not the person's rule. A page's own
37
+ * chat never receives itself (Arman, 2026-10-01 / 2026-10-21).
38
+ */
39
+ own?: boolean;
34
40
  }
35
41
  /** The non-person layers for one value — what the table's layer breakdown shows. */
36
42
  interface ContextRuleLayers {
@@ -31,6 +31,12 @@ interface ContextSurfaceLayer {
31
31
  auto_context?: boolean | null;
32
32
  /** `inlineUpTo` / envelope `max_inline_chars` / DB column. */
33
33
  max_inline_chars?: number | null;
34
+ /**
35
+ * The value is withheld because this conversation IS the page (`page_context` mode `own`):
36
+ * nothing re-opens it — not the agent's Context Policy, not the person's rule. A page's own
37
+ * chat never receives itself (Arman, 2026-10-01 / 2026-10-21).
38
+ */
39
+ own?: boolean;
34
40
  }
35
41
  /** The non-person layers for one value — what the table's layer breakdown shows. */
36
42
  interface ContextRuleLayers {
@@ -62,7 +62,11 @@ function resolveContextRule(input) {
62
62
  include = false;
63
63
  includeBy = "agent";
64
64
  }
65
- if (user && typeof user.include === "boolean") {
65
+ const ownLocked = !identity && surface?.own === true;
66
+ if (ownLocked) {
67
+ include = false;
68
+ includeBy = "page";
69
+ } else if (user && typeof user.include === "boolean") {
66
70
  include = user.include;
67
71
  includeBy = "you";
68
72
  }
@@ -182,6 +186,7 @@ function includeChanges(rows, include, cap) {
182
186
  const changes = [];
183
187
  for (const row of rows) {
184
188
  if (row.include === include) continue;
189
+ if (row.layers?.surface?.own === true) continue;
185
190
  const next = { ...row.userRule ?? {} };
186
191
  if (inheritedInclude(row, cap) === include) delete next.include;
187
192
  else next.include = include;
@@ -733,6 +738,7 @@ function ContextRuleRow({
733
738
  size: "sm",
734
739
  "aria-label": `Include ${row.label}`,
735
740
  checked: row.include,
741
+ disabled: row.layers?.surface?.own === true,
736
742
  onCheckedChange: (next) => onChange(row.key, row.surfaceKey, withInclude(row, next))
737
743
  }
738
744
  ) }),
@@ -47,9 +47,9 @@ module.exports = __toCommonJS(mandates_exports);
47
47
 
48
48
  // mandates/keys.generated.ts
49
49
  var MANDATE_KEYS_META = {
50
- aidreamRevision: "1e5f26b1bb6d67f1553566c6c38b25e570e2a765",
50
+ aidreamRevision: "268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab",
51
51
  generatorVersion: "1.0.0",
52
- packageVersion: "0.59.215",
52
+ packageVersion: "0.59.217",
53
53
  retireWindow: 3
54
54
  };
55
55
  var MANDATE_KEYS = {
@@ -6,9 +6,9 @@
6
6
  * from the IN-PROCESS mandate declarations — the same `declared_mandates()` the
7
7
  * boot sync writes to the database from. There is no hand list anywhere.
8
8
  *
9
- * aidream revision: 1e5f26b1bb6d67f1553566c6c38b25e570e2a765
9
+ * aidream revision: 268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab
10
10
  * generator version: 1.0.0
11
- * package version: 0.59.215
11
+ * package version: 0.59.217
12
12
  * active keys: 612
13
13
  * retiring keys: 5
14
14
  *
@@ -19,9 +19,9 @@
19
19
  */
20
20
  /** Where this module came from — surfaced so a report can name the exact build. */
21
21
  declare const MANDATE_KEYS_META: {
22
- readonly aidreamRevision: "1e5f26b1bb6d67f1553566c6c38b25e570e2a765";
22
+ readonly aidreamRevision: "268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab";
23
23
  readonly generatorVersion: "1.0.0";
24
- readonly packageVersion: "0.59.215";
24
+ readonly packageVersion: "0.59.217";
25
25
  readonly retireWindow: 3;
26
26
  };
27
27
  /** Every declared mandate key, by THE IDENTIFIER RULE. */
@@ -6,9 +6,9 @@
6
6
  * from the IN-PROCESS mandate declarations — the same `declared_mandates()` the
7
7
  * boot sync writes to the database from. There is no hand list anywhere.
8
8
  *
9
- * aidream revision: 1e5f26b1bb6d67f1553566c6c38b25e570e2a765
9
+ * aidream revision: 268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab
10
10
  * generator version: 1.0.0
11
- * package version: 0.59.215
11
+ * package version: 0.59.217
12
12
  * active keys: 612
13
13
  * retiring keys: 5
14
14
  *
@@ -19,9 +19,9 @@
19
19
  */
20
20
  /** Where this module came from — surfaced so a report can name the exact build. */
21
21
  declare const MANDATE_KEYS_META: {
22
- readonly aidreamRevision: "1e5f26b1bb6d67f1553566c6c38b25e570e2a765";
22
+ readonly aidreamRevision: "268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab";
23
23
  readonly generatorVersion: "1.0.0";
24
- readonly packageVersion: "0.59.215";
24
+ readonly packageVersion: "0.59.217";
25
25
  readonly retireWindow: 3;
26
26
  };
27
27
  /** Every declared mandate key, by THE IDENTIFIER RULE. */
@@ -1,8 +1,8 @@
1
1
  // mandates/keys.generated.ts
2
2
  var MANDATE_KEYS_META = {
3
- aidreamRevision: "1e5f26b1bb6d67f1553566c6c38b25e570e2a765",
3
+ aidreamRevision: "268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab",
4
4
  generatorVersion: "1.0.0",
5
- packageVersion: "0.59.215",
5
+ packageVersion: "0.59.217",
6
6
  retireWindow: 3
7
7
  };
8
8
  var MANDATE_KEYS = {
@@ -1005,6 +1005,117 @@ function createModelRecords(config) {
1005
1005
  inFlight.set(key, p);
1006
1006
  return p;
1007
1007
  };
1008
+ async function readFullModel(modelId) {
1009
+ const relation = await isSignedIn() ? "model_config" : "model_public";
1010
+ const { data, error } = await ai().from(relation).select(MODEL_CONFIG_COLUMNS).eq("id", modelId).maybeSingle();
1011
+ if (error) throw error;
1012
+ const row = data;
1013
+ if (!row) throw new Error(`AI model ${modelId} is not available (ai.${relation} has no such row)`);
1014
+ if (!row.id || !row.name) throw new Error(`ai.${relation} returned a row without id/name for model ${modelId}`);
1015
+ const read = {
1016
+ ...row,
1017
+ id: row.id,
1018
+ name: row.name,
1019
+ controls: row.controls ?? null,
1020
+ capabilities: requireCanonicalCapabilities(row.capabilities, { modelId: row.id, modelName: row.name })
1021
+ };
1022
+ return normalizeModel(read);
1023
+ }
1024
+ async function readOfferings(modelId) {
1025
+ const q = ai().from("model_offering").select("model_id, offering_id, served_via, served_via_endpoint_id, priority");
1026
+ const { data, error } = await (modelId ? q.eq("model_id", modelId) : q);
1027
+ if (error) throw error;
1028
+ return data ?? [];
1029
+ }
1030
+ async function rereadKnown() {
1031
+ const known = state2;
1032
+ const jobs = [];
1033
+ const fail = (code, message, context) => errorSink({ code, message: `Catalog refresh: ${message}`, ...context ? { context } : {} });
1034
+ if (known.fetchScope === "active" || known.fetchScope === "all") {
1035
+ jobs.push(
1036
+ readOptions().then(
1037
+ (rows3) => update((s3) => {
1038
+ const entities = { ...s3.entities };
1039
+ for (const row of rows3) {
1040
+ const existing = entities[row.id];
1041
+ entities[row.id] = { ...emptyRecord(), ...existing, ...row, _fetchType: existing?._fetchType === "full" ? "full" : "options" };
1042
+ }
1043
+ return withIdLists({ ...s3, entities, lastFetched: Date.now(), error: null });
1044
+ }),
1045
+ (err) => fail("model-records-refresh-options-failed", messageOf(err))
1046
+ )
1047
+ );
1048
+ }
1049
+ for (const [id, record] of Object.entries(known.entities)) {
1050
+ if (record._fetchType !== "full") continue;
1051
+ jobs.push(
1052
+ readFullModel(id).then(
1053
+ (fresh) => update((s3) => {
1054
+ const existing = s3.entities[fresh.id];
1055
+ return withIdLists({ ...s3, entities: { ...s3.entities, [fresh.id]: { ...emptyRecord(), ...existing, ...fresh, maker: fresh.maker ?? existing?.maker ?? null, _fetchType: "full" } } });
1056
+ }),
1057
+ (err) => fail("model-records-refresh-model-failed", `${id}: ${messageOf(err)}`, { modelId: id })
1058
+ )
1059
+ );
1060
+ }
1061
+ for (const id of Object.keys(known.identityById)) {
1062
+ jobs.push(
1063
+ Promise.resolve(ai().from("model_definition").select("id, name, common_name, is_deprecated").eq("id", id).maybeSingle()).then(
1064
+ ({ data, error }) => {
1065
+ if (error) throw error;
1066
+ if (data) update((s3) => ({ ...s3, identityById: { ...s3.identityById, [id]: data } }));
1067
+ }
1068
+ ).catch((err) => fail("model-records-refresh-identity-failed", `${id}: ${messageOf(err)}`, { modelId: id }))
1069
+ );
1070
+ }
1071
+ if (known.modelClassStatus === "succeeded") {
1072
+ jobs.push(
1073
+ readOfferings().then(
1074
+ (rows3) => update((s3) => ({ ...s3, modelClassIndex: buildModelClassIndex(rows3) })),
1075
+ (err) => fail("model-records-refresh-classes-failed", messageOf(err))
1076
+ )
1077
+ );
1078
+ } else {
1079
+ for (const id of classesReadFor) {
1080
+ jobs.push(
1081
+ readOfferings(id).then(
1082
+ (rows3) => update((s3) => {
1083
+ if (s3.modelClassStatus === "succeeded") return s3;
1084
+ const slice = buildModelClassIndex(rows3);
1085
+ const base = s3.modelClassIndex ?? { classesByModelId: {}, classOfOffering: {} };
1086
+ return { ...s3, modelClassIndex: { classesByModelId: { ...base.classesByModelId, ...slice.classesByModelId }, classOfOffering: { ...base.classOfOffering, ...slice.classOfOffering } } };
1087
+ }),
1088
+ (err) => fail("model-records-refresh-classes-failed", `${id}: ${messageOf(err)}`, { modelId: id })
1089
+ )
1090
+ );
1091
+ }
1092
+ }
1093
+ for (const [key, cfg] of Object.entries(known.classConfigByOffering)) {
1094
+ const offeringId = key.slice(cfg.modelId.length + 1);
1095
+ jobs.push(
1096
+ Promise.resolve(ai().rpc("resolve_model_config", { p_model_id: cfg.modelId, p_offering_id: offeringId })).then(
1097
+ ({ data, error }) => {
1098
+ if (error) throw error;
1099
+ const next = data && typeof data === "object" && !Array.isArray(data) ? data : null;
1100
+ update((s3) => ({ ...s3, classConfigByOffering: { ...s3.classConfigByOffering, [key]: { modelId: cfg.modelId, controls: next?.controls ?? null, constraints: next?.constraints ?? null } } }));
1101
+ }
1102
+ ).catch((err) => fail("model-records-refresh-class-config-failed", `${key}: ${messageOf(err)}`))
1103
+ );
1104
+ }
1105
+ await Promise.all(jobs);
1106
+ }
1107
+ let refreshing = null;
1108
+ let refreshAgain = null;
1109
+ function refreshAll() {
1110
+ if (refreshing) return refreshAgain ??= refreshing.then(() => {
1111
+ refreshAgain = null;
1112
+ return refreshAll();
1113
+ });
1114
+ refreshing = rereadKnown().finally(() => {
1115
+ refreshing = null;
1116
+ });
1117
+ return refreshing;
1118
+ }
1008
1119
  async function readOptions() {
1009
1120
  const { data: offeringRows, error: offeringError } = await ai().from("model_offering").select("model_id");
1010
1121
  if (offeringError) throw offeringError;
@@ -1070,20 +1181,7 @@ function createModelRecords(config) {
1070
1181
  return { ...s3, detailStatusById: { ...s3.detailStatusById, [modelId]: "loading" }, detailErrorById };
1071
1182
  });
1072
1183
  try {
1073
- const relation = await isSignedIn() ? "model_config" : "model_public";
1074
- const { data, error } = await ai().from(relation).select(MODEL_CONFIG_COLUMNS).eq("id", modelId).maybeSingle();
1075
- if (error) throw error;
1076
- const row = data;
1077
- if (!row) throw new Error(`AI model ${modelId} is not available (ai.${relation} has no such row)`);
1078
- if (!row.id || !row.name) throw new Error(`ai.${relation} returned a row without id/name for model ${modelId}`);
1079
- const read = {
1080
- ...row,
1081
- id: row.id,
1082
- name: row.name,
1083
- controls: row.controls ?? null,
1084
- capabilities: requireCanonicalCapabilities(row.capabilities, { modelId: row.id, modelName: row.name })
1085
- };
1086
- const record = normalizeModel(read);
1184
+ const record = await readFullModel(modelId);
1087
1185
  update((s3) => {
1088
1186
  const existing = s3.entities[record.id];
1089
1187
  const detailErrorById = { ...s3.detailErrorById };
@@ -1207,6 +1305,7 @@ function createModelRecords(config) {
1207
1305
  }
1208
1306
  });
1209
1307
  },
1308
+ refresh: () => refreshAll(),
1210
1309
  seedModel({ modelId, config: config2, offerings }) {
1211
1310
  if (!modelId) return;
1212
1311
  if (!classesReadFor.has(modelId) && state2.modelClassStatus !== "succeeded") {
@@ -671,6 +671,14 @@ interface ModelRecords {
671
671
  loadModelClasses(modelId: string): Promise<void>;
672
672
  /** The durable label of a model outside the options list. Never throws. */
673
673
  loadIdentity(modelId: string): Promise<void>;
674
+ /**
675
+ * The catalog changed upstream (a `catalog_changed` notice: an admin renamed, offered or retired a model):
676
+ * re-read everything this instance already read — the options list, every full record, every identity,
677
+ * the class index and slices, every pinned class's controls — and swap each in when it answers. The old
678
+ * rows stay on screen until then (no blank pill, no flicker); a failed re-read keeps them and reports
679
+ * through the error sink. Reads nothing that was never read. Never throws.
680
+ */
681
+ refresh(): Promise<void>;
674
682
  /**
675
683
  * Answer `loadModel` + `loadModelClasses` for one model from rows a batched
676
684
  * route read already returned (`chat.route_seed`): its `ai.model_config` row
@@ -671,6 +671,14 @@ interface ModelRecords {
671
671
  loadModelClasses(modelId: string): Promise<void>;
672
672
  /** The durable label of a model outside the options list. Never throws. */
673
673
  loadIdentity(modelId: string): Promise<void>;
674
+ /**
675
+ * The catalog changed upstream (a `catalog_changed` notice: an admin renamed, offered or retired a model):
676
+ * re-read everything this instance already read — the options list, every full record, every identity,
677
+ * the class index and slices, every pinned class's controls — and swap each in when it answers. The old
678
+ * rows stay on screen until then (no blank pill, no flicker); a failed re-read keeps them and reports
679
+ * through the error sink. Reads nothing that was never read. Never throws.
680
+ */
681
+ refresh(): Promise<void>;
674
682
  /**
675
683
  * Answer `loadModel` + `loadModelClasses` for one model from rows a batched
676
684
  * route read already returned (`chat.route_seed`): its `ai.model_config` row
@@ -847,6 +847,117 @@ function createModelRecords(config) {
847
847
  inFlight.set(key, p);
848
848
  return p;
849
849
  };
850
+ async function readFullModel(modelId) {
851
+ const relation = await isSignedIn() ? "model_config" : "model_public";
852
+ const { data, error } = await ai().from(relation).select(MODEL_CONFIG_COLUMNS).eq("id", modelId).maybeSingle();
853
+ if (error) throw error;
854
+ const row = data;
855
+ if (!row) throw new Error(`AI model ${modelId} is not available (ai.${relation} has no such row)`);
856
+ if (!row.id || !row.name) throw new Error(`ai.${relation} returned a row without id/name for model ${modelId}`);
857
+ const read = {
858
+ ...row,
859
+ id: row.id,
860
+ name: row.name,
861
+ controls: row.controls ?? null,
862
+ capabilities: requireCanonicalCapabilities(row.capabilities, { modelId: row.id, modelName: row.name })
863
+ };
864
+ return normalizeModel(read);
865
+ }
866
+ async function readOfferings(modelId) {
867
+ const q = ai().from("model_offering").select("model_id, offering_id, served_via, served_via_endpoint_id, priority");
868
+ const { data, error } = await (modelId ? q.eq("model_id", modelId) : q);
869
+ if (error) throw error;
870
+ return data ?? [];
871
+ }
872
+ async function rereadKnown() {
873
+ const known = state2;
874
+ const jobs = [];
875
+ const fail = (code, message, context) => errorSink({ code, message: `Catalog refresh: ${message}`, ...context ? { context } : {} });
876
+ if (known.fetchScope === "active" || known.fetchScope === "all") {
877
+ jobs.push(
878
+ readOptions().then(
879
+ (rows3) => update((s3) => {
880
+ const entities = { ...s3.entities };
881
+ for (const row of rows3) {
882
+ const existing = entities[row.id];
883
+ entities[row.id] = { ...emptyRecord(), ...existing, ...row, _fetchType: existing?._fetchType === "full" ? "full" : "options" };
884
+ }
885
+ return withIdLists({ ...s3, entities, lastFetched: Date.now(), error: null });
886
+ }),
887
+ (err) => fail("model-records-refresh-options-failed", messageOf(err))
888
+ )
889
+ );
890
+ }
891
+ for (const [id, record] of Object.entries(known.entities)) {
892
+ if (record._fetchType !== "full") continue;
893
+ jobs.push(
894
+ readFullModel(id).then(
895
+ (fresh) => update((s3) => {
896
+ const existing = s3.entities[fresh.id];
897
+ return withIdLists({ ...s3, entities: { ...s3.entities, [fresh.id]: { ...emptyRecord(), ...existing, ...fresh, maker: fresh.maker ?? existing?.maker ?? null, _fetchType: "full" } } });
898
+ }),
899
+ (err) => fail("model-records-refresh-model-failed", `${id}: ${messageOf(err)}`, { modelId: id })
900
+ )
901
+ );
902
+ }
903
+ for (const id of Object.keys(known.identityById)) {
904
+ jobs.push(
905
+ Promise.resolve(ai().from("model_definition").select("id, name, common_name, is_deprecated").eq("id", id).maybeSingle()).then(
906
+ ({ data, error }) => {
907
+ if (error) throw error;
908
+ if (data) update((s3) => ({ ...s3, identityById: { ...s3.identityById, [id]: data } }));
909
+ }
910
+ ).catch((err) => fail("model-records-refresh-identity-failed", `${id}: ${messageOf(err)}`, { modelId: id }))
911
+ );
912
+ }
913
+ if (known.modelClassStatus === "succeeded") {
914
+ jobs.push(
915
+ readOfferings().then(
916
+ (rows3) => update((s3) => ({ ...s3, modelClassIndex: buildModelClassIndex(rows3) })),
917
+ (err) => fail("model-records-refresh-classes-failed", messageOf(err))
918
+ )
919
+ );
920
+ } else {
921
+ for (const id of classesReadFor) {
922
+ jobs.push(
923
+ readOfferings(id).then(
924
+ (rows3) => update((s3) => {
925
+ if (s3.modelClassStatus === "succeeded") return s3;
926
+ const slice = buildModelClassIndex(rows3);
927
+ const base = s3.modelClassIndex ?? { classesByModelId: {}, classOfOffering: {} };
928
+ return { ...s3, modelClassIndex: { classesByModelId: { ...base.classesByModelId, ...slice.classesByModelId }, classOfOffering: { ...base.classOfOffering, ...slice.classOfOffering } } };
929
+ }),
930
+ (err) => fail("model-records-refresh-classes-failed", `${id}: ${messageOf(err)}`, { modelId: id })
931
+ )
932
+ );
933
+ }
934
+ }
935
+ for (const [key, cfg] of Object.entries(known.classConfigByOffering)) {
936
+ const offeringId = key.slice(cfg.modelId.length + 1);
937
+ jobs.push(
938
+ Promise.resolve(ai().rpc("resolve_model_config", { p_model_id: cfg.modelId, p_offering_id: offeringId })).then(
939
+ ({ data, error }) => {
940
+ if (error) throw error;
941
+ const next = data && typeof data === "object" && !Array.isArray(data) ? data : null;
942
+ update((s3) => ({ ...s3, classConfigByOffering: { ...s3.classConfigByOffering, [key]: { modelId: cfg.modelId, controls: next?.controls ?? null, constraints: next?.constraints ?? null } } }));
943
+ }
944
+ ).catch((err) => fail("model-records-refresh-class-config-failed", `${key}: ${messageOf(err)}`))
945
+ );
946
+ }
947
+ await Promise.all(jobs);
948
+ }
949
+ let refreshing = null;
950
+ let refreshAgain = null;
951
+ function refreshAll() {
952
+ if (refreshing) return refreshAgain ??= refreshing.then(() => {
953
+ refreshAgain = null;
954
+ return refreshAll();
955
+ });
956
+ refreshing = rereadKnown().finally(() => {
957
+ refreshing = null;
958
+ });
959
+ return refreshing;
960
+ }
850
961
  async function readOptions() {
851
962
  const { data: offeringRows, error: offeringError } = await ai().from("model_offering").select("model_id");
852
963
  if (offeringError) throw offeringError;
@@ -912,20 +1023,7 @@ function createModelRecords(config) {
912
1023
  return { ...s3, detailStatusById: { ...s3.detailStatusById, [modelId]: "loading" }, detailErrorById };
913
1024
  });
914
1025
  try {
915
- const relation = await isSignedIn() ? "model_config" : "model_public";
916
- const { data, error } = await ai().from(relation).select(MODEL_CONFIG_COLUMNS).eq("id", modelId).maybeSingle();
917
- if (error) throw error;
918
- const row = data;
919
- if (!row) throw new Error(`AI model ${modelId} is not available (ai.${relation} has no such row)`);
920
- if (!row.id || !row.name) throw new Error(`ai.${relation} returned a row without id/name for model ${modelId}`);
921
- const read = {
922
- ...row,
923
- id: row.id,
924
- name: row.name,
925
- controls: row.controls ?? null,
926
- capabilities: requireCanonicalCapabilities(row.capabilities, { modelId: row.id, modelName: row.name })
927
- };
928
- const record = normalizeModel(read);
1026
+ const record = await readFullModel(modelId);
929
1027
  update((s3) => {
930
1028
  const existing = s3.entities[record.id];
931
1029
  const detailErrorById = { ...s3.detailErrorById };
@@ -1049,6 +1147,7 @@ function createModelRecords(config) {
1049
1147
  }
1050
1148
  });
1051
1149
  },
1150
+ refresh: () => refreshAll(),
1052
1151
  seedModel({ modelId, config: config2, offerings }) {
1053
1152
  if (!modelId) return;
1054
1153
  if (!classesReadFor.has(modelId) && state2.modelClassStatus !== "succeeded") {
@@ -331,6 +331,14 @@ interface ModelRecords {
331
331
  loadModelClasses(modelId: string): Promise<void>;
332
332
  /** The durable label of a model outside the options list. Never throws. */
333
333
  loadIdentity(modelId: string): Promise<void>;
334
+ /**
335
+ * The catalog changed upstream (a `catalog_changed` notice: an admin renamed, offered or retired a model):
336
+ * re-read everything this instance already read — the options list, every full record, every identity,
337
+ * the class index and slices, every pinned class's controls — and swap each in when it answers. The old
338
+ * rows stay on screen until then (no blank pill, no flicker); a failed re-read keeps them and reports
339
+ * through the error sink. Reads nothing that was never read. Never throws.
340
+ */
341
+ refresh(): Promise<void>;
334
342
  /**
335
343
  * Answer `loadModel` + `loadModelClasses` for one model from rows a batched
336
344
  * route read already returned (`chat.route_seed`): its `ai.model_config` row
@@ -331,6 +331,14 @@ interface ModelRecords {
331
331
  loadModelClasses(modelId: string): Promise<void>;
332
332
  /** The durable label of a model outside the options list. Never throws. */
333
333
  loadIdentity(modelId: string): Promise<void>;
334
+ /**
335
+ * The catalog changed upstream (a `catalog_changed` notice: an admin renamed, offered or retired a model):
336
+ * re-read everything this instance already read — the options list, every full record, every identity,
337
+ * the class index and slices, every pinned class's controls — and swap each in when it answers. The old
338
+ * rows stay on screen until then (no blank pill, no flicker); a failed re-read keeps them and reports
339
+ * through the error sink. Reads nothing that was never read. Never throws.
340
+ */
341
+ refresh(): Promise<void>;
334
342
  /**
335
343
  * Answer `loadModel` + `loadModelClasses` for one model from rows a batched
336
344
  * route read already returned (`chat.route_seed`): its `ai.model_config` row
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/agents",
3
- "version": "0.59.215",
3
+ "version": "0.59.217",
4
4
  "description": "Portable AI Matrx agent stream protocol, event projection, and safe result-presentation primitives.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -510,13 +510,13 @@
510
510
  "dependencies": {
511
511
  "@ai-matrx/alchemy": "latest",
512
512
  "@ai-matrx/api": "latest",
513
- "@ai-matrx/associations": "latest",
514
513
  "@ai-matrx/content-ir": "latest",
514
+ "@ai-matrx/associations": "latest",
515
515
  "@ai-matrx/data": "latest",
516
516
  "@ai-matrx/design-system": "latest",
517
517
  "@ai-matrx/diff": "latest",
518
- "@ai-matrx/icons": "latest",
519
- "@ai-matrx/kit": "latest"
518
+ "@ai-matrx/kit": "latest",
519
+ "@ai-matrx/icons": "latest"
520
520
  },
521
521
  "peerDependencies": {
522
522
  "react": ">=18.0.0",
@@ -561,5 +561,5 @@
561
561
  "verify:tarball": "node ./scripts/verify-tarball.mjs",
562
562
  "typegen:self-test": "node scripts/typegen/drop-guard.self-test.mjs && node scripts/typegen/check-api-types-fresh.mjs --self-test"
563
563
  },
564
- "gitHead": "1e5f26b1bb6d67f1553566c6c38b25e570e2a765"
564
+ "gitHead": "268d0c2f1f0b2ea129ee132098cb3e7b9184a2ab"
565
565
  }