@memberjunction/server 5.23.0 → 5.24.0

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 (33) hide show
  1. package/dist/config.d.ts.map +1 -1
  2. package/dist/config.js +11 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/generated/generated.d.ts +462 -0
  5. package/dist/generated/generated.d.ts.map +1 -1
  6. package/dist/generated/generated.js +16584 -13956
  7. package/dist/generated/generated.js.map +1 -1
  8. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  9. package/dist/generic/RunViewResolver.js.map +1 -1
  10. package/dist/resolvers/AutotagPipelineResolver.d.ts +10 -1
  11. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  12. package/dist/resolvers/AutotagPipelineResolver.js +97 -13
  13. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  14. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  15. package/dist/resolvers/FetchEntityVectorsResolver.js +6 -2
  16. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  17. package/dist/resolvers/RunAIAgentResolver.d.ts +21 -0
  18. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  19. package/dist/resolvers/RunAIAgentResolver.js +73 -33
  20. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  21. package/dist/resolvers/SearchKnowledgeResolver.d.ts +42 -1
  22. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  23. package/dist/resolvers/SearchKnowledgeResolver.js +239 -13
  24. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  25. package/package.json +63 -63
  26. package/src/__tests__/search-knowledge-tags.test.ts +415 -0
  27. package/src/config.ts +11 -0
  28. package/src/generated/generated.ts +1807 -1
  29. package/src/generic/RunViewResolver.ts +1 -0
  30. package/src/resolvers/AutotagPipelineResolver.ts +99 -10
  31. package/src/resolvers/FetchEntityVectorsResolver.ts +6 -2
  32. package/src/resolvers/RunAIAgentResolver.ts +95 -56
  33. package/src/resolvers/SearchKnowledgeResolver.ts +270 -13
@@ -765,6 +765,7 @@ export class RunViewResolver extends ResolverBase {
765
765
  for (const [index, data] of rawData.entries()) {
766
766
  // EntityName is backfilled by RunViewsGeneric when ViewID/ViewName was used
767
767
  const entity = input[index].EntityName ? provider.Entities.find((e) => e.Name === input[index].EntityName) : null;
768
+
768
769
  const returnData: any[] = this.processRawData(data.Results, entity ? entity.ID : null, entity);
769
770
 
770
771
  results.push({
@@ -1,6 +1,7 @@
1
- import { Resolver, Mutation, Ctx, ObjectType, Field } from 'type-graphql';
1
+ import { Resolver, Mutation, Ctx, Arg, ObjectType, Field } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, LogStatus } from '@memberjunction/core';
3
+ import { LogError, LogStatus, Metadata } from '@memberjunction/core';
4
+ import { MJContentProcessRunEntity } from '@memberjunction/core-entities';
4
5
  import { ResolverBase } from '../generic/ResolverBase.js';
5
6
  import { ActionEngineServer } from '@memberjunction/actions';
6
7
  import { PubSubManager } from '../generic/PubSubManager.js';
@@ -28,6 +29,8 @@ export class AutotagPipelineResult {
28
29
  export class AutotagPipelineResolver extends ResolverBase {
29
30
  @Mutation(() => AutotagPipelineResult)
30
31
  async RunAutotagPipeline(
32
+ @Arg('contentSourceIDs', () => [String], { nullable: true }) contentSourceIDs: string[] | undefined,
33
+ @Arg('forceReprocess', { nullable: true }) forceReprocess: boolean | undefined,
31
34
  @Ctx() { userPayload }: AppContext = {} as AppContext
32
35
  ): Promise<AutotagPipelineResult> {
33
36
  try {
@@ -40,7 +43,7 @@ export class AutotagPipelineResolver extends ResolverBase {
40
43
  LogStatus(`RunAutotagPipeline: starting pipeline ${pipelineRunID}`);
41
44
 
42
45
  // Fire-and-forget: start the pipeline in the background and return immediately
43
- this.runPipelineInBackground(pipelineRunID, currentUser);
46
+ this.runPipelineInBackground(pipelineRunID, currentUser, contentSourceIDs, forceReprocess);
44
47
 
45
48
  return {
46
49
  Success: true,
@@ -64,7 +67,9 @@ export class AutotagPipelineResolver extends ResolverBase {
64
67
  */
65
68
  private async runPipelineInBackground(
66
69
  pipelineRunID: string,
67
- currentUser: import('@memberjunction/core').UserInfo
70
+ currentUser: import('@memberjunction/core').UserInfo,
71
+ contentSourceIDs?: string[],
72
+ forceReprocess?: boolean
68
73
  ): Promise<void> {
69
74
  const startTime = Date.now();
70
75
  try {
@@ -89,16 +94,24 @@ export class AutotagPipelineResolver extends ResolverBase {
89
94
  this.publishProgress(pipelineRunID, 'autotag', total, pct, startTime, currentItem || `${processed}/${total} items`);
90
95
  };
91
96
 
92
- // Run with both Autotag=1 and Vectorize=1: the action will tag and embed in parallel
97
+ // Build action params
98
+ const actionParams: Array<{ Name: string; Value: unknown; Type: 'Input' | 'Output' | 'Both' }> = [
99
+ { Name: 'Autotag', Value: 1, Type: 'Input' },
100
+ { Name: 'Vectorize', Value: 1, Type: 'Input' },
101
+ { Name: '__progressCallback', Value: progressCallback, Type: 'Input' }
102
+ ];
103
+ if (contentSourceIDs && contentSourceIDs.length > 0) {
104
+ actionParams.push({ Name: 'ContentSourceIDs', Value: contentSourceIDs, Type: 'Input' });
105
+ }
106
+ if (forceReprocess) {
107
+ actionParams.push({ Name: 'ForceReprocess', Value: 1, Type: 'Input' });
108
+ }
109
+
93
110
  const result = await ActionEngineServer.Instance.RunAction({
94
111
  Action: action,
95
112
  ContextUser: currentUser,
96
113
  Filters: [],
97
- Params: [
98
- { Name: 'Autotag', Value: 1, Type: 'Input' },
99
- { Name: 'Vectorize', Value: 1, Type: 'Input' },
100
- { Name: '__progressCallback', Value: progressCallback, Type: 'Input' }
101
- ]
114
+ Params: actionParams
102
115
  });
103
116
 
104
117
  // Stage: vectorize complete
@@ -118,6 +131,82 @@ export class AutotagPipelineResolver extends ResolverBase {
118
131
  }
119
132
  }
120
133
 
134
+ /**
135
+ * Pause a running classification pipeline by setting CancellationRequested on the process run.
136
+ * The engine checks this flag between batches and pauses gracefully.
137
+ */
138
+ @Mutation(() => AutotagPipelineResult)
139
+ async PauseClassificationPipeline(
140
+ @Arg('processRunID') processRunID: string,
141
+ @Ctx() { userPayload }: AppContext = {} as AppContext
142
+ ): Promise<AutotagPipelineResult> {
143
+ try {
144
+ const currentUser = this.GetUserFromPayload(userPayload);
145
+ if (!currentUser) {
146
+ return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
147
+ }
148
+
149
+ const md = new Metadata();
150
+ const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
151
+ const loaded = await run.Load(processRunID);
152
+ if (!loaded) {
153
+ return { Success: false, Status: 'Error', ErrorMessage: `Process run ${processRunID} not found` };
154
+ }
155
+
156
+ run.CancellationRequested = true;
157
+ await run.Save();
158
+
159
+ LogStatus(`PauseClassificationPipeline: Pause requested for run ${processRunID}`);
160
+ return { Success: true, Status: 'PauseRequested' };
161
+ } catch (error) {
162
+ const msg = error instanceof Error ? error.message : String(error);
163
+ return { Success: false, Status: 'Error', ErrorMessage: msg };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Resume a paused classification pipeline from its last completed offset.
169
+ */
170
+ @Mutation(() => AutotagPipelineResult)
171
+ async ResumeClassificationPipeline(
172
+ @Arg('processRunID') processRunID: string,
173
+ @Ctx() { userPayload }: AppContext = {} as AppContext
174
+ ): Promise<AutotagPipelineResult> {
175
+ try {
176
+ const currentUser = this.GetUserFromPayload(userPayload);
177
+ if (!currentUser) {
178
+ return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
179
+ }
180
+
181
+ const md = new Metadata();
182
+ const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
183
+ const loaded = await run.Load(processRunID);
184
+ if (!loaded) {
185
+ return { Success: false, Status: 'Error', ErrorMessage: `Process run ${processRunID} not found` };
186
+ }
187
+
188
+ if (run.Status !== 'Paused') {
189
+ return { Success: false, Status: 'Error', ErrorMessage: `Run is not paused (Status: ${run.Status})` };
190
+ }
191
+
192
+ // Reset cancellation flag and set status back to Running
193
+ run.CancellationRequested = false;
194
+ run.Status = 'Running';
195
+ await run.Save();
196
+
197
+ // Fire-and-forget: resume pipeline in background from the last offset
198
+ const pipelineRunID = uuidv4();
199
+ LogStatus(`ResumeClassificationPipeline: Resuming run ${processRunID} from offset ${run.LastProcessedOffset}`);
200
+
201
+ this.runPipelineInBackground(pipelineRunID, currentUser, undefined, undefined);
202
+
203
+ return { Success: true, Status: 'Resumed', PipelineRunID: pipelineRunID };
204
+ } catch (error) {
205
+ const msg = error instanceof Error ? error.message : String(error);
206
+ return { Success: false, Status: 'Error', ErrorMessage: msg };
207
+ }
208
+ }
209
+
121
210
  /**
122
211
  * Publish a progress update to the PipelineProgress subscription topic.
123
212
  */
@@ -90,13 +90,17 @@ export class FetchEntityVectorsResolver extends ResolverBase {
90
90
  // but the metadata filter ensures we only get vectors for this entity.
91
91
  const entityName = entityDoc.Entity;
92
92
  const dimensions = vectorIndex.Dimensions || 1536; // fall back to common embedding size
93
- const zeroVector = new Array(dimensions).fill(0);
93
+ // Use a tiny uniform vector instead of zero — cosine similarity is undefined
94
+ // for a zero vector (division by zero), causing Pinecone to return 0 matches.
95
+ // A uniform vector has equal similarity to all vectors, giving us an unbiased
96
+ // listing that respects the metadata filter.
97
+ const uniformVector = new Array(dimensions).fill(1.0 / Math.sqrt(dimensions));
94
98
 
95
99
  const metadataFilter: Record<string, unknown> = { Entity: { $eq: entityName } };
96
100
 
97
101
  const queryResponse = await vectorDBInstance.QueryIndex({
98
102
  id: vectorIndex.Name, // index name (stripped before Pinecone query)
99
- vector: zeroVector,
103
+ vector: uniformVector,
100
104
  topK: limit,
101
105
  includeMetadata: true,
102
106
  includeValues: true,
@@ -361,7 +361,9 @@ export class RunAIAgentResolver extends ResolverBase {
361
361
  createArtifacts: boolean = false,
362
362
  createNotification: boolean = false,
363
363
  sourceArtifactId?: string,
364
- sourceArtifactVersionId?: string
364
+ sourceArtifactVersionId?: string,
365
+ /** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
366
+ conversationId?: string
365
367
  ): Promise<AIAgentRunResult> {
366
368
  const startTime = Date.now();
367
369
 
@@ -418,6 +420,7 @@ export class RunAIAgentResolver extends ResolverBase {
418
420
  }
419
421
  }, {
420
422
  conversationDetailId: conversationDetailId, // Use existing if provided
423
+ conversationId: conversationId, // LATENCY OPT #2: pre-resolved to skip redundant load in AgentRunner
421
424
  userMessage: userMessage, // Provide user message when conversationDetailId not provided
422
425
  createArtifacts: createArtifacts || false,
423
426
  sourceArtifactId: sourceArtifactId
@@ -435,37 +438,53 @@ export class RunAIAgentResolver extends ResolverBase {
435
438
 
436
439
  const executionTime = Date.now() - startTime;
437
440
 
438
- // Sync feedback request if this is a continuation run (user responded via conversation)
441
+ // LATENCY OPTIMIZATION (Opt #6): These three post-execution operations are independent
442
+ // of each other — none reads the output of another. Previously they ran sequentially,
443
+ // adding their latencies together (~50ms total). Now they run in parallel via Promise.all,
444
+ // so we only pay the cost of the slowest one.
445
+ //
446
+ // 1. syncFeedbackRequestFromConversation — links a prior Chat-step feedback request to
447
+ // the new agent run so the conversation thread stays coherent.
448
+ // 2. sendFeedbackRequestNotification — sends an in-app/email/SMS notification when the
449
+ // agent paused for human input (Chat step).
450
+ // 3. createCompletionNotification — sends an in-app/email/SMS notification that the
451
+ // agent finished and created an artifact.
452
+ const postExecutionOps: Promise<void>[] = [];
453
+
439
454
  if (lastRunId && result.agentRun?.ID) {
440
- await this.syncFeedbackRequestFromConversation(
441
- lastRunId,
442
- result.agentRun.ID,
443
- userMessage,
444
- currentUser
455
+ postExecutionOps.push(
456
+ this.syncFeedbackRequestFromConversation(lastRunId, result.agentRun.ID, userMessage, currentUser)
445
457
  );
446
458
  }
447
459
 
448
- // Send notification if agent created a feedback request (Chat step)
449
460
  if (result.feedbackRequestId) {
450
- await this.sendFeedbackRequestNotification(result, currentUser, pubSub, userPayload);
461
+ postExecutionOps.push(
462
+ this.sendFeedbackRequestNotification(result, currentUser, pubSub, userPayload)
463
+ );
451
464
  }
452
465
 
453
- // Create notification if enabled and artifact was created successfully
454
466
  if (createNotification && result.success && artifactInfo && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
455
- await this.createCompletionNotification(
456
- result.agentRun,
457
- {
458
- artifactId: artifactInfo.artifactId,
459
- versionId: artifactInfo.versionId,
460
- versionNumber: artifactInfo.versionNumber
461
- },
462
- finalConversationDetailId,
463
- currentUser,
464
- pubSub,
465
- userPayload
467
+ postExecutionOps.push(
468
+ this.createCompletionNotification(
469
+ result.agentRun,
470
+ {
471
+ artifactId: artifactInfo.artifactId,
472
+ versionId: artifactInfo.versionId,
473
+ versionNumber: artifactInfo.versionNumber
474
+ },
475
+ conversationResult.conversationId,
476
+ finalConversationDetailId,
477
+ currentUser,
478
+ pubSub,
479
+ userPayload
480
+ )
466
481
  );
467
482
  }
468
483
 
484
+ if (postExecutionOps.length > 0) {
485
+ await Promise.all(postExecutionOps);
486
+ }
487
+
469
488
  // Create sanitized payload for JSON serialization
470
489
  const sanitizedResult = this.sanitizeAgentResult(result);
471
490
  const returnResult = JSON.stringify(sanitizedResult);
@@ -680,34 +699,31 @@ export class RunAIAgentResolver extends ResolverBase {
680
699
  * Create a user notification for agent completion with artifact
681
700
  * Notification includes navigation link back to the conversation
682
701
  */
702
+ /**
703
+ * LATENCY OPTIMIZATION (Opt #2): Now accepts conversationId directly instead of
704
+ * conversationDetailId. Previously this method loaded a ConversationDetail entity
705
+ * from the DB solely to extract its ConversationID field for building a URL — a
706
+ * redundant ~50ms DB round-trip since the caller already resolved conversationId
707
+ * when loading conversation history.
708
+ */
683
709
  private async createCompletionNotification(
684
710
  agentRun: MJAIAgentRunEntityExtended,
685
711
  artifactInfo: { artifactId: string; versionId: string; versionNumber: number },
712
+ conversationId: string,
686
713
  conversationDetailId: string,
687
714
  contextUser: UserInfo,
688
715
  pubSub: PubSubEngine,
689
716
  userPayload: UserPayload
690
717
  ): Promise<void> {
691
718
  try {
692
- const md = new Metadata();
693
-
694
719
  // Get agent info for notification message
695
720
  await AIEngine.Instance.Config(false, contextUser);
696
721
  const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, agentRun.AgentID));
697
722
  const agentName = agent?.Name || 'Agent';
698
723
 
699
- // Load conversation detail to get conversation info
700
- const detail = await md.GetEntityObject<MJConversationDetailEntity>(
701
- 'MJ: Conversation Details',
702
- contextUser
703
- );
704
- if (!(await detail.Load(conversationDetailId))) {
705
- throw new Error(`Failed to load conversation detail ${conversationDetailId}`);
706
- }
707
-
708
724
  // Build conversation URL for email/SMS templates
709
725
  const baseUrl = process.env.APP_BASE_URL || 'http://localhost:4201';
710
- const conversationUrl = `${baseUrl}/conversations/${detail.ConversationID}?artifact=${artifactInfo.artifactId}`;
726
+ const conversationUrl = `${baseUrl}/conversations/${conversationId}?artifact=${artifactInfo.artifactId}`;
711
727
 
712
728
  // Craft message based on versioning
713
729
  const message = artifactInfo.versionNumber > 1
@@ -724,7 +740,7 @@ export class RunAIAgentResolver extends ResolverBase {
724
740
  message: message,
725
741
  resourceConfiguration: {
726
742
  type: 'conversation',
727
- conversationId: detail.ConversationID,
743
+ conversationId: conversationId,
728
744
  messageId: conversationDetailId,
729
745
  artifactId: artifactInfo.artifactId,
730
746
  versionId: artifactInfo.versionId,
@@ -755,7 +771,7 @@ export class RunAIAgentResolver extends ResolverBase {
755
771
  action: 'create',
756
772
  title: `${agentName} completed your request`,
757
773
  message: message,
758
- conversationId: detail.ConversationID
774
+ conversationId: conversationId
759
775
  })
760
776
  });
761
777
 
@@ -918,9 +934,24 @@ export class RunAIAgentResolver extends ResolverBase {
918
934
  }
919
935
 
920
936
  try {
937
+ // LATENCY OPTIMIZATION (Opt #2 + #3): Load ConversationDetail once here to extract
938
+ // conversationId, then pass it downstream. Previously this record was loaded multiple
939
+ // times: once in loadConversationHistoryWithAttachments (just to get conversationId),
940
+ // once in AgentRunner (same reason), and once in createCompletionNotification. Now we
941
+ // load it a single time and thread conversationId through the call chain.
942
+ const md = new Metadata();
943
+ const currentDetail = await md.GetEntityObject<MJConversationDetailEntity>(
944
+ 'MJ: Conversation Details',
945
+ currentUser
946
+ );
947
+ if (!await currentDetail.Load(conversationDetailId)) {
948
+ throw new Error(`Conversation detail ${conversationDetailId} not found`);
949
+ }
950
+ const conversationId = currentDetail.ConversationID;
951
+
921
952
  // Load conversation history with attachments from DB
922
953
  const messages = await this.loadConversationHistoryWithAttachments(
923
- conversationDetailId,
954
+ conversationId,
924
955
  currentUser,
925
956
  maxHistoryMessages || 20
926
957
  );
@@ -935,7 +966,7 @@ export class RunAIAgentResolver extends ResolverBase {
935
966
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
936
967
  data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
937
968
  conversationDetailId, createArtifacts || false, createNotification || false,
938
- sourceArtifactId, sourceArtifactVersionId
969
+ sourceArtifactId, sourceArtifactVersionId, conversationId
939
970
  );
940
971
 
941
972
  LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
@@ -965,7 +996,8 @@ export class RunAIAgentResolver extends ResolverBase {
965
996
  createArtifacts || false,
966
997
  createNotification || false,
967
998
  sourceArtifactId,
968
- sourceArtifactVersionId
999
+ sourceArtifactVersionId,
1000
+ conversationId // LATENCY OPT #2: pass pre-resolved conversationId
969
1001
  );
970
1002
  } catch (error) {
971
1003
  const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
@@ -1178,14 +1210,16 @@ export class RunAIAgentResolver extends ResolverBase {
1178
1210
  createArtifacts: boolean = false,
1179
1211
  createNotification: boolean = false,
1180
1212
  sourceArtifactId?: string,
1181
- sourceArtifactVersionId?: string
1213
+ sourceArtifactVersionId?: string,
1214
+ /** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
1215
+ conversationId?: string
1182
1216
  ): void {
1183
1217
  // Execute in background - errors are handled within, not propagated
1184
1218
  this.executeAIAgent(
1185
1219
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
1186
1220
  data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
1187
1221
  configurationId, conversationDetailId, createArtifacts, createNotification,
1188
- sourceArtifactId, sourceArtifactVersionId
1222
+ sourceArtifactId, sourceArtifactVersionId, conversationId
1189
1223
  ).catch((error: unknown) => {
1190
1224
  // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
1191
1225
  // so this would only fire for truly unexpected errors).
@@ -1210,34 +1244,39 @@ export class RunAIAgentResolver extends ResolverBase {
1210
1244
  /**
1211
1245
  * Load conversation history with attachments from database.
1212
1246
  * Builds ChatMessage[] with multimodal content blocks for attachments.
1247
+ *
1248
+ * LATENCY OPTIMIZATIONS (plans/agent-latency-optimization.md — Opts #3 and #8):
1249
+ *
1250
+ * Opt #3: This method now accepts conversationId directly instead of conversationDetailId.
1251
+ * Previously it loaded a ConversationDetail entity object just to extract its ConversationID
1252
+ * field — a redundant DB round-trip (~40ms) since the caller already has this information.
1253
+ * The caller (RunAIAgentFromConversationDetail) now loads the ConversationDetail once and
1254
+ * passes conversationId down.
1255
+ *
1256
+ * Opt #8: Switched from ResultType 'entity_object' to 'simple' with explicit Fields.
1257
+ * The history query only needs ID, Role, and Message from each ConversationDetail record.
1258
+ * Using 'entity_object' created full BaseEntity instances with getters/setters, dirty tracking,
1259
+ * and validation — none of which are needed for read-only history assembly. The 'simple' result
1260
+ * type returns plain JS objects, reducing per-record overhead (~30ms total savings).
1213
1261
  */
1214
1262
  private async loadConversationHistoryWithAttachments(
1215
- conversationDetailId: string,
1263
+ conversationId: string,
1216
1264
  contextUser: UserInfo,
1217
1265
  maxMessages: number
1218
1266
  ): Promise<ChatMessage[]> {
1219
- const md = new Metadata();
1220
1267
  const rv = new RunView();
1221
1268
  const attachmentService = getAttachmentService();
1222
1269
 
1223
- // Load the current conversation detail to get the conversation ID
1224
- const currentDetail = await md.GetEntityObject<MJConversationDetailEntity>(
1225
- 'MJ: Conversation Details',
1226
- contextUser
1227
- );
1228
- if (!await currentDetail.Load(conversationDetailId)) {
1229
- throw new Error(`Conversation detail ${conversationDetailId} not found`);
1230
- }
1231
-
1232
- const conversationId = currentDetail.ConversationID;
1233
-
1234
- // Load recent conversation details (messages) for this conversation
1235
- const detailsResult = await rv.RunView<MJConversationDetailEntity>({
1270
+ // Load recent conversation details (messages) for this conversation.
1271
+ // Only fetch the three fields we actually use — ID for attachment lookups,
1272
+ // Role for message routing, Message for content.
1273
+ const detailsResult = await rv.RunView<{ ID: string; Role: string; Message: string }>({
1236
1274
  EntityName: 'MJ: Conversation Details',
1237
1275
  ExtraFilter: `ConversationID='${conversationId}'`,
1238
1276
  OrderBy: '__mj_CreatedAt DESC',
1239
1277
  MaxRows: maxMessages,
1240
- ResultType: 'entity_object'
1278
+ Fields: ['ID', 'Role', 'Message'],
1279
+ ResultType: 'simple'
1241
1280
  }, contextUser);
1242
1281
 
1243
1282
  if (!detailsResult.Success || !detailsResult.Results) {