@memberjunction/core-entities 6.2.0-edge.4 → 6.2.0-edge.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/custom/ConversationWriteAccess.d.ts +5 -0
  3. package/dist/custom/ConversationWriteAccess.d.ts.map +1 -1
  4. package/dist/custom/ConversationWriteAccess.js +6 -6
  5. package/dist/custom/ConversationWriteAccess.js.map +1 -1
  6. package/dist/custom/MJConversationDetailEntityExtended.d.ts +46 -4
  7. package/dist/custom/MJConversationDetailEntityExtended.d.ts.map +1 -1
  8. package/dist/custom/MJConversationDetailEntityExtended.js +185 -28
  9. package/dist/custom/MJConversationDetailEntityExtended.js.map +1 -1
  10. package/dist/custom/MJThemeEntityExtended.d.ts +13 -0
  11. package/dist/custom/MJThemeEntityExtended.d.ts.map +1 -0
  12. package/dist/custom/MJThemeEntityExtended.js +37 -0
  13. package/dist/custom/MJThemeEntityExtended.js.map +1 -0
  14. package/dist/engines/InstanceConfigEngine.d.ts.map +1 -1
  15. package/dist/engines/InstanceConfigEngine.js +13 -3
  16. package/dist/engines/InstanceConfigEngine.js.map +1 -1
  17. package/dist/engines/PermissionEngine.d.ts +5 -4
  18. package/dist/engines/PermissionEngine.d.ts.map +1 -1
  19. package/dist/engines/PermissionEngine.js +7 -5
  20. package/dist/engines/PermissionEngine.js.map +1 -1
  21. package/dist/engines/QueryEngine.d.ts +81 -6
  22. package/dist/engines/QueryEngine.d.ts.map +1 -1
  23. package/dist/engines/QueryEngine.js +283 -4
  24. package/dist/engines/QueryEngine.js.map +1 -1
  25. package/dist/engines/UserInfoEngine.d.ts +48 -2
  26. package/dist/engines/UserInfoEngine.d.ts.map +1 -1
  27. package/dist/engines/UserInfoEngine.js +65 -8
  28. package/dist/engines/UserInfoEngine.js.map +1 -1
  29. package/dist/engines/conversations.d.ts +19 -1
  30. package/dist/engines/conversations.d.ts.map +1 -1
  31. package/dist/engines/conversations.js +125 -22
  32. package/dist/engines/conversations.js.map +1 -1
  33. package/dist/generated/entities/__mj.d.ts +1762 -44
  34. package/dist/generated/entities/__mj.d.ts.map +1 -1
  35. package/dist/generated/entities/__mj.js +2231 -18
  36. package/dist/generated/entities/__mj.js.map +1 -1
  37. package/dist/generated/remote_operations.d.ts +17 -5
  38. package/dist/generated/remote_operations.d.ts.map +1 -1
  39. package/dist/generated/remote_operations.js.map +1 -1
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -0
  43. package/dist/index.js.map +1 -1
  44. package/package.json +6 -5
@@ -44,14 +44,20 @@ function sameId(a, b) {
44
44
  * Helper: parse a raw ConversationDetailComplete row into typed arrays.
45
45
  */
46
46
  export function ParseConversationDetailComplete(queryResult) {
47
+ const rawArtifacts = queryResult.ArtifactsJSON
48
+ ? JSON.parse(queryResult.ArtifactsJSON)
49
+ : [];
50
+ const artifacts = rawArtifacts.slice().sort((a, b) => {
51
+ const aTime = ToEpochMs(a.VersionCreatedAt) ?? ToEpochMs(a.__mj_CreatedAt) ?? 0;
52
+ const bTime = ToEpochMs(b.VersionCreatedAt) ?? ToEpochMs(b.__mj_CreatedAt) ?? 0;
53
+ return aTime - bTime;
54
+ });
47
55
  return {
48
56
  ...queryResult,
49
57
  agentRuns: queryResult.AgentRunsJSON
50
58
  ? JSON.parse(queryResult.AgentRunsJSON)
51
59
  : [],
52
- artifacts: queryResult.ArtifactsJSON
53
- ? JSON.parse(queryResult.ArtifactsJSON)
54
- : [],
60
+ artifacts,
55
61
  ratings: queryResult.RatingsJSON
56
62
  ? JSON.parse(queryResult.RatingsJSON)
57
63
  : []
@@ -113,12 +119,63 @@ function detailIdInFilter(detailIds) {
113
119
  function idInFilter(ids) {
114
120
  return `ID IN (${quoteIdList(ids)})`;
115
121
  }
116
- /** Keys agent runs by the detail they belong to. */
122
+ /**
123
+ * Priority tier for choosing which `AIAgentRun` represents a `ConversationDetail` in the UI
124
+ * (gear icon -> "View Agent Run", token/cost stats, duration):
125
+ * - Tier 0: Root primary orchestrating run (`ParentRunID == null` and `ContinuationDepth == 0`)
126
+ * - Tier 1: Root post-workflow continuation synthesis run (`ParentRunID == null` and `ContinuationDepth > 0`)
127
+ * - Tier 2: Child / sub-agent run (`ParentRunID != null`)
128
+ */
129
+ function agentRunPriorityTier(run) {
130
+ if (run.ParentRunID) {
131
+ return 2;
132
+ }
133
+ return (run.ContinuationDepth ?? 0) > 0 ? 1 : 0;
134
+ }
135
+ /**
136
+ * Returns true when `candidate` should replace `existing` as the representative agent run
137
+ * for a single `ConversationDetailID`. Lower tier wins (primary root run beats continuation
138
+ * synthesis run, which beats child sub-agent run); within the same tier, a strictly newer run
139
+ * wins (so user retries of a message replace the earlier run while DESC-ordered queries keep
140
+ * the first row when timestamps are tied or omitted in test fixtures).
141
+ */
142
+ export function ShouldReplaceDetailAgentRun(existing, candidate) {
143
+ if (!existing) {
144
+ return true;
145
+ }
146
+ if (existing.ID && candidate.ID && UUIDsEqual(existing.ID, candidate.ID)) {
147
+ return true;
148
+ }
149
+ const existingTier = agentRunPriorityTier(existing);
150
+ const candidateTier = agentRunPriorityTier(candidate);
151
+ if (candidateTier !== existingTier) {
152
+ return candidateTier < existingTier;
153
+ }
154
+ const existingTime = ToEpochMs(existing.__mj_CreatedAt ?? existing.StartedAt) ?? 0;
155
+ const candidateTime = ToEpochMs(candidate.__mj_CreatedAt ?? candidate.StartedAt) ?? 0;
156
+ if (candidateTime !== existingTime) {
157
+ return candidateTime > existingTime;
158
+ }
159
+ return false;
160
+ }
161
+ function selectPreferredDetailAgentRun(runs) {
162
+ let best;
163
+ for (const run of runs) {
164
+ if (ShouldReplaceDetailAgentRun(best, run)) {
165
+ best = run;
166
+ }
167
+ }
168
+ return best;
169
+ }
170
+ /** Keys agent runs by the detail they belong to, preferring the primary root orchestrating run. */
117
171
  function groupAgentRunsByDetailId(runs) {
118
172
  const byDetailId = new Map();
119
173
  for (const run of runs) {
120
174
  if (run.ConversationDetailID) {
121
- byDetailId.set(run.ConversationDetailID, run);
175
+ const existing = byDetailId.get(run.ConversationDetailID);
176
+ if (ShouldReplaceDetailAgentRun(existing, run)) {
177
+ byDetailId.set(run.ConversationDetailID, run);
178
+ }
122
179
  }
123
180
  }
124
181
  return byDetailId;
@@ -1036,14 +1093,29 @@ export class ConversationEngine extends BaseEngine {
1036
1093
  throw new Error('Conversation not found');
1037
1094
  }
1038
1095
  }
1096
+ const previousValues = {};
1097
+ for (const key of Object.keys(updates)) {
1098
+ previousValues[key] = conversation[key];
1099
+ }
1039
1100
  this.mergeDataOntoRecord(conversation, updates);
1101
+ // Emit immediately after mutating the shared cached entity so Angular subscribers
1102
+ // (e.g. ConversationListComponent) run change detection synchronously rather than
1103
+ // seeing an uncommitted mutation during the async Save() round-trip (NG0100).
1104
+ this._conversations$.next([...this._conversations$.value]);
1040
1105
  this._selfMutating = true;
1041
1106
  try {
1042
1107
  const saved = await conversation.Save();
1043
1108
  if (!saved) {
1109
+ this.mergeDataOntoRecord(conversation, previousValues);
1110
+ this._conversations$.next([...this._conversations$.value]);
1044
1111
  throw new Error(conversation.LatestResult?.Message || 'Failed to update conversation');
1045
1112
  }
1046
1113
  }
1114
+ catch (err) {
1115
+ this.mergeDataOntoRecord(conversation, previousValues);
1116
+ this._conversations$.next([...this._conversations$.value]);
1117
+ throw err;
1118
+ }
1047
1119
  finally {
1048
1120
  this._selfMutating = false;
1049
1121
  }
@@ -1531,14 +1603,16 @@ export class ConversationEngine extends BaseEngine {
1531
1603
  if (!row.ID)
1532
1604
  continue;
1533
1605
  const parsed = ParseConversationDetailComplete(row);
1534
- // Agent runs
1535
- if (parsed.agentRuns.length > 0) {
1536
- const agentRunData = parsed.agentRuns[0];
1606
+ // Agent runs (prefer primary root orchestrating run over continuation synthesis or child runs)
1607
+ const agentRunData = selectPreferredDetailAgentRun(parsed.agentRuns);
1608
+ if (agentRunData) {
1537
1609
  const agentRun = await md.GetEntityObject('MJ: AI Agent Runs', contextUser);
1538
1610
  agentRun.LoadFromData({
1539
1611
  ID: agentRunData.ID,
1540
1612
  AgentID: agentRunData.AgentID,
1541
1613
  Agent: agentRunData.Agent,
1614
+ ParentRunID: agentRunData.ParentRunID ?? null,
1615
+ ContinuationDepth: agentRunData.ContinuationDepth ?? 0,
1542
1616
  Status: agentRunData.Status,
1543
1617
  __mj_CreatedAt: agentRunData.__mj_CreatedAt,
1544
1618
  __mj_UpdatedAt: agentRunData.__mj_UpdatedAt,
@@ -1600,6 +1674,7 @@ export class ConversationEngine extends BaseEngine {
1600
1674
  {
1601
1675
  EntityName: 'MJ: Conversation Detail Artifacts',
1602
1676
  ExtraFilter: `${byDetail} AND Direction='Output'`,
1677
+ OrderBy: '__mj_CreatedAt ASC',
1603
1678
  ResultType: 'simple'
1604
1679
  }
1605
1680
  ], contextUser);
@@ -1670,6 +1745,14 @@ export class ConversationEngine extends BaseEngine {
1670
1745
  list.push(mergeArtifactJSON(junction, version, artifact));
1671
1746
  byDetailId.set(junction.ConversationDetailID, list);
1672
1747
  }
1748
+ for (const [detailId, list] of byDetailId) {
1749
+ list.sort((a, b) => {
1750
+ const aTime = ToEpochMs(a.VersionCreatedAt) ?? ToEpochMs(a.__mj_CreatedAt) ?? 0;
1751
+ const bTime = ToEpochMs(b.VersionCreatedAt) ?? ToEpochMs(b.__mj_CreatedAt) ?? 0;
1752
+ return aTime - bTime;
1753
+ });
1754
+ byDetailId.set(detailId, list);
1755
+ }
1673
1756
  return byDetailId;
1674
1757
  }
1675
1758
  /** Reads the artifact rows behind a window's artifact versions. */
@@ -1756,12 +1839,14 @@ export class ConversationEngine extends BaseEngine {
1756
1839
  // check for `LoadFromData` before calling it, and hydrate a real entity when
1757
1840
  // we only have plain data. Mirrors the pattern in
1758
1841
  // `conversation-chat-area.component.ts#onMessageComplete` (line ~909).
1759
- if (parsed.agentRuns.length > 0) {
1760
- const agentRunData = parsed.agentRuns[0];
1842
+ const agentRunData = selectPreferredDetailAgentRun(parsed.agentRuns);
1843
+ if (agentRunData) {
1761
1844
  const mergeFields = {
1762
1845
  ID: agentRunData.ID,
1763
1846
  AgentID: agentRunData.AgentID,
1764
1847
  Agent: agentRunData.Agent,
1848
+ ParentRunID: agentRunData.ParentRunID ?? null,
1849
+ ContinuationDepth: agentRunData.ContinuationDepth ?? 0,
1765
1850
  Status: agentRunData.Status,
1766
1851
  __mj_CreatedAt: agentRunData.__mj_CreatedAt,
1767
1852
  __mj_UpdatedAt: agentRunData.__mj_UpdatedAt,
@@ -1771,15 +1856,17 @@ export class ConversationEngine extends BaseEngine {
1771
1856
  ConversationDetailID: agentRunData.ConversationDetailID
1772
1857
  };
1773
1858
  const existingRun = existing.AgentRunsByDetailId.get(row.ID);
1774
- const existingRunIsEntity = !!existingRun && typeof existingRun.LoadFromData === 'function';
1775
- if (existingRunIsEntity) {
1776
- existingRun.LoadFromData(mergeFields);
1777
- }
1778
- else {
1779
- // Either no existing run, or existing value is plain JSON — hydrate fresh.
1780
- const newRun = await md.GetEntityObject('MJ: AI Agent Runs', contextUser);
1781
- newRun.LoadFromData(existingRun ? { ...existingRun, ...mergeFields } : agentRunData);
1782
- existing.AgentRunsByDetailId.set(row.ID, newRun);
1859
+ if (ShouldReplaceDetailAgentRun(existingRun, agentRunData)) {
1860
+ const existingRunIsEntity = !!existingRun && typeof existingRun.LoadFromData === 'function';
1861
+ if (existingRunIsEntity) {
1862
+ existingRun.LoadFromData(mergeFields);
1863
+ }
1864
+ else {
1865
+ // Either no existing run, or existing value is plain JSON — hydrate fresh.
1866
+ const newRun = await md.GetEntityObject('MJ: AI Agent Runs', contextUser);
1867
+ newRun.LoadFromData(existingRun ? { ...existingRun, ...mergeFields } : agentRunData);
1868
+ existing.AgentRunsByDetailId.set(row.ID, newRun);
1869
+ }
1783
1870
  }
1784
1871
  }
1785
1872
  // Replace artifacts per-detail (plain data, cheap to replace)
@@ -2523,7 +2610,9 @@ export class ConversationEngine extends BaseEngine {
2523
2610
  return this._detailCache.get(key)?.AgentRunsByDetailId ?? new Map();
2524
2611
  }
2525
2612
  /**
2526
- * Adds or updates an agent run in the cache for a specific detail.
2613
+ * Adds or updates an agent run in the cache for a specific detail, honoring
2614
+ * {@link ShouldReplaceDetailAgentRun} so a continuation/sub-agent run never overwrites
2615
+ * the primary root orchestrating run for that detail.
2527
2616
  *
2528
2617
  * @param conversationId - The conversation ID
2529
2618
  * @param detailId - The detail ID the agent run is associated with
@@ -2532,8 +2621,22 @@ export class ConversationEngine extends BaseEngine {
2532
2621
  SetAgentRunForDetail(conversationId, detailId, agentRun) {
2533
2622
  const key = NormalizeUUID(conversationId);
2534
2623
  const cached = this._detailCache.get(key);
2535
- if (cached) {
2536
- cached.AgentRunsByDetailId.set(detailId, agentRun);
2624
+ if (!cached) {
2625
+ return;
2626
+ }
2627
+ let targetDetailKey = detailId;
2628
+ let existing = cached.AgentRunsByDetailId.get(detailId);
2629
+ if (!existing) {
2630
+ for (const [cachedDetailId, cachedRun] of cached.AgentRunsByDetailId) {
2631
+ if (UUIDsEqual(cachedDetailId, detailId)) {
2632
+ targetDetailKey = cachedDetailId;
2633
+ existing = cachedRun;
2634
+ break;
2635
+ }
2636
+ }
2637
+ }
2638
+ if (ShouldReplaceDetailAgentRun(existing, agentRun)) {
2639
+ cached.AgentRunsByDetailId.set(targetDetailKey, agentRun);
2537
2640
  }
2538
2641
  }
2539
2642
  // ========================================================================