@memberjunction/server 5.29.0 → 5.30.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.
@@ -1,4 +1,4 @@
1
- import { Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
1
+ import { DatabaseProviderBase, Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
2
2
  import { MJTaskEntity, MJTaskDependencyEntity, MJTaskTypeEntity, MJConversationDetailEntity, MJArtifactEntity, MJArtifactVersionEntity, MJConversationDetailArtifactEntity, MJUserNotificationEntity } from '@memberjunction/core-entities';
3
3
  import { AgentRunner } from '@memberjunction/ai-agents';
4
4
  import { ChatMessageRole } from '@memberjunction/ai';
@@ -107,24 +107,18 @@ export class TaskOrchestrator {
107
107
  const md = new Metadata();
108
108
  const tempIdToRealId = new Map<string, string>();
109
109
 
110
- // Create parent workflow task
110
+ // Build the parent task, deduplicate the incoming task defs, and resolve agents
111
+ // BEFORE opening the transaction so all preparatory work (cache lookups, agent
112
+ // resolution RunViews) happens outside the critical section.
111
113
  const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
112
114
  parentTask.Name = taskGraph.workflowName;
113
115
  parentTask.Description = taskGraph.reasoning || 'AI-orchestrated workflow';
114
116
  parentTask.TypeID = taskTypeId;
115
117
  parentTask.EnvironmentID = environmentId;
116
- parentTask.ConversationDetailID = conversationDetailId; // Parent links to conversation
117
- parentTask.Status = 'In Progress'; // Workflow is in progress
118
+ parentTask.ConversationDetailID = conversationDetailId;
119
+ parentTask.Status = 'In Progress';
118
120
  parentTask.PercentComplete = 0;
119
121
 
120
- const parentSaved = await parentTask.Save();
121
- if (!parentSaved) {
122
- throw new Error('Failed to create parent workflow task');
123
- }
124
-
125
- LogStatus(`Created parent workflow task: ${parentTask.Name} (${parentTask.ID})`);
126
-
127
- // Deduplicate tasks by tempId (LLM sometimes returns duplicates)
128
122
  const seenTempIds = new Set<string>();
129
123
  const uniqueTasks = taskGraph.tasks.filter(task => {
130
124
  if (seenTempIds.has(task.tempId)) {
@@ -135,66 +129,81 @@ export class TaskOrchestrator {
135
129
  return true;
136
130
  });
137
131
 
138
- LogStatus(`Creating ${uniqueTasks.length} unique child tasks (${taskGraph.tasks.length - uniqueTasks.length} duplicates filtered)`);
132
+ LogStatus(`Preparing parent + ${uniqueTasks.length} unique child tasks (${taskGraph.tasks.length - uniqueTasks.length} duplicates filtered)`);
139
133
 
140
- // Create all child tasks
134
+ const resolvedTasks: Array<{ def: TaskDefinition; agentId: string }> = [];
141
135
  for (const taskDef of uniqueTasks) {
142
- const task = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
143
-
144
- // Find agent by name
145
136
  const agent = await this.findAgentByName(taskDef.agentName);
146
137
  if (!agent) {
147
138
  LogError(`Agent not found: ${taskDef.agentName}`);
148
139
  continue;
149
140
  }
141
+ resolvedTasks.push({ def: taskDef, agentId: agent.ID });
142
+ }
150
143
 
151
- task.Name = taskDef.name;
152
- task.Description = taskDef.description;
153
- task.TypeID = taskTypeId;
154
- task.EnvironmentID = environmentId;
155
- task.ParentID = parentTask.ID; // Link to parent task
156
- task.ConversationDetailID = conversationDetailId; // Link to conversation so agent runs can be tracked
157
- task.AgentID = agent.ID;
158
- task.Status = 'Pending';
159
- task.PercentComplete = 0;
160
-
161
- // Store input payload if provided
162
- if (taskDef.inputPayload) {
163
- const metadata = {
164
- inputPayload: taskDef.inputPayload,
165
- tempId: taskDef.tempId
166
- };
167
- // Store in a well-known format at the end of description
168
- task.Description = `${taskDef.description}\n\n__TASK_METADATA__\n${JSON.stringify(metadata)}`;
144
+ // Persist parent + children + dependency graph in one transaction
145
+ const provider = Metadata.Provider as DatabaseProviderBase;
146
+ await provider.BeginTransaction();
147
+ try {
148
+ if (!await parentTask.Save()) {
149
+ throw new Error(`Failed to create parent workflow task: ${parentTask.LatestResult?.CompleteMessage ?? 'unknown error'}`);
169
150
  }
151
+ LogStatus(`Created parent workflow task: ${parentTask.Name} (${parentTask.ID})`);
152
+
153
+ for (const { def, agentId } of resolvedTasks) {
154
+ const task = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
155
+ task.Name = def.name;
156
+ task.Description = def.description;
157
+ task.TypeID = taskTypeId;
158
+ task.EnvironmentID = environmentId;
159
+ task.ParentID = parentTask.ID;
160
+ task.ConversationDetailID = conversationDetailId;
161
+ task.AgentID = agentId;
162
+ task.Status = 'Pending';
163
+ task.PercentComplete = 0;
164
+
165
+ if (def.inputPayload) {
166
+ const metadata = {
167
+ inputPayload: def.inputPayload,
168
+ tempId: def.tempId
169
+ };
170
+ task.Description = `${def.description}\n\n__TASK_METADATA__\n${JSON.stringify(metadata)}`;
171
+ }
170
172
 
171
- const saved = await task.Save();
172
- if (saved) {
173
- tempIdToRealId.set(taskDef.tempId, task.ID);
173
+ if (!await task.Save()) {
174
+ throw new Error(`Failed to create child task '${def.name}': ${task.LatestResult?.CompleteMessage ?? 'unknown error'}`);
175
+ }
176
+ tempIdToRealId.set(def.tempId, task.ID);
174
177
  LogStatus(`Created child task: ${task.Name} (${task.ID}) under parent ${parentTask.ID}`);
175
178
  }
176
- }
177
179
 
178
- // Create dependencies between child tasks
179
- for (const taskDef of uniqueTasks) {
180
- const taskId = tempIdToRealId.get(taskDef.tempId);
181
- if (!taskId) continue;
182
-
183
- for (const dependsOnTempId of taskDef.dependsOn) {
184
- const dependsOnId = tempIdToRealId.get(dependsOnTempId);
185
- if (!dependsOnId) {
186
- LogError(`Dependency not found: ${dependsOnTempId}`);
187
- continue;
180
+ for (const { def } of resolvedTasks) {
181
+ const taskId = tempIdToRealId.get(def.tempId);
182
+ if (!taskId) continue;
183
+
184
+ for (const dependsOnTempId of def.dependsOn) {
185
+ const dependsOnId = tempIdToRealId.get(dependsOnTempId);
186
+ if (!dependsOnId) {
187
+ LogError(`Dependency not found: ${dependsOnTempId}`);
188
+ continue;
189
+ }
190
+
191
+ const dependency = await md.GetEntityObject<MJTaskDependencyEntity>('MJ: Task Dependencies', this.contextUser);
192
+ dependency.TaskID = taskId;
193
+ dependency.DependsOnTaskID = dependsOnId;
194
+ dependency.DependencyType = 'Prerequisite';
195
+
196
+ if (!await dependency.Save()) {
197
+ throw new Error(`Failed to create task dependency (${taskId} -> ${dependsOnId}): ${dependency.LatestResult?.CompleteMessage ?? 'unknown error'}`);
198
+ }
199
+ LogStatus(`Created dependency: Task ${taskId} depends on ${dependsOnId}`);
188
200
  }
189
-
190
- const dependency = await md.GetEntityObject<MJTaskDependencyEntity>('MJ: Task Dependencies', this.contextUser);
191
- dependency.TaskID = taskId;
192
- dependency.DependsOnTaskID = dependsOnId;
193
- dependency.DependencyType = 'Prerequisite';
194
-
195
- await dependency.Save();
196
- LogStatus(`Created dependency: Task ${taskId} depends on ${dependsOnId}`);
197
201
  }
202
+
203
+ await provider.CommitTransaction();
204
+ } catch (txErr) {
205
+ await provider.RollbackTransaction();
206
+ throw txErr;
198
207
  }
199
208
 
200
209
  return {
@@ -698,23 +707,22 @@ export class TaskOrchestrator {
698
707
  agent: MJAIAgentEntityExtended,
699
708
  taskName: string
700
709
  ): Promise<void> {
701
- try {
702
- const md = new Metadata();
710
+ const md = new Metadata();
711
+ const provider = Metadata.Provider as DatabaseProviderBase;
703
712
 
713
+ await provider.BeginTransaction();
714
+ try {
704
715
  // Create Artifact header
705
716
  const artifact = await md.GetEntityObject<MJArtifactEntity>('MJ: Artifacts', this.contextUser);
706
717
  artifact.Name = `${agent.Name} - ${taskName} - ${new Date().toLocaleString()}`;
707
718
  artifact.Description = `Artifact generated by ${agent.Name} for task: ${taskName} (${output.type})`;
708
719
 
709
- // Use agent's DefaultArtifactTypeID if available, otherwise fall back to JSON
710
720
  const defaultArtifactTypeId = (agent as any).DefaultArtifactTypeID;
711
721
  artifact.TypeID = defaultArtifactTypeId || this.JSON_ARTIFACT_TYPE_ID;
712
722
 
713
723
  artifact.UserID = this.contextUser.ID;
714
724
  artifact.EnvironmentID = (this.contextUser as any).EnvironmentID || 'F51358F3-9447-4176-B313-BF8025FD8D09';
715
725
 
716
- // Set visibility based on agent's ArtifactCreationMode
717
- // Will compile after CodeGen adds the new fields
718
726
  const creationMode = agent.ArtifactCreationMode;
719
727
  if (creationMode === 'System Only') {
720
728
  artifact.Visibility = 'System Only';
@@ -723,51 +731,36 @@ export class TaskOrchestrator {
723
731
  artifact.Visibility = 'Always';
724
732
  }
725
733
 
726
- const artifactSaved = await artifact.Save();
727
- if (!artifactSaved) {
728
- LogError('Failed to save artifact');
729
- return;
734
+ if (!await artifact.Save()) {
735
+ throw new Error(`Failed to save artifact: ${artifact.LatestResult?.CompleteMessage ?? 'unknown error'}`);
730
736
  }
731
-
732
737
  LogStatus(`Created artifact: ${artifact.Name} (${artifact.ID})`);
733
738
 
734
739
  // Create Artifact Version with content
735
740
  const version = await md.GetEntityObject<MJArtifactVersionEntity>('MJ: Artifact Versions', this.contextUser);
736
741
  version.ArtifactID = artifact.ID;
737
742
  version.VersionNumber = 1;
738
-
739
- // Store content based on output type
740
- if (output.type === 'message') {
741
- version.Content = output.content;
742
- } else {
743
- version.Content = JSON.stringify(output.content, null, 2);
744
- }
745
-
743
+ version.Content = output.type === 'message' ? output.content : JSON.stringify(output.content, null, 2);
746
744
  version.UserID = this.contextUser.ID;
747
745
 
748
- const versionSaved = await version.Save();
749
- if (!versionSaved) {
750
- LogError('Failed to save artifact version');
751
- return;
746
+ if (!await version.Save()) {
747
+ throw new Error(`Failed to save artifact version: ${version.LatestResult?.CompleteMessage ?? 'unknown error'}`);
752
748
  }
753
-
754
749
  LogStatus(`Created artifact version: ${version.ID}`);
755
750
 
756
- // Check for extracted Name attribute and update artifact with better name
751
+ // If extraction produced a better name, update the artifact in the same transaction
757
752
  const nameAttr = (version as any).Attributes?.find((attr: any) =>
758
753
  attr.StandardProperty === 'name' || attr.Name?.toLowerCase() === 'name'
759
754
  );
760
755
 
761
- // Check for valid name value (not null, not empty, not string "null")
762
756
  let extractedName = nameAttr?.Value?.trim();
763
757
  if (extractedName && extractedName.toLowerCase() !== 'null') {
764
- // Strip surrounding quotes (double or single) from start and end
765
758
  extractedName = extractedName.replace(/^["']|["']$/g, '');
766
-
767
759
  artifact.Name = extractedName;
768
- if (await artifact.Save()) {
769
- LogStatus(`✨ Updated artifact name to: ${artifact.Name}`);
760
+ if (!await artifact.Save()) {
761
+ throw new Error(`Failed to update artifact name: ${artifact.LatestResult?.CompleteMessage ?? 'unknown error'}`);
770
762
  }
763
+ LogStatus(`✨ Updated artifact name to: ${artifact.Name}`);
771
764
  }
772
765
 
773
766
  // Create M2M relationship linking artifact to conversation detail
@@ -777,17 +770,17 @@ export class TaskOrchestrator {
777
770
  );
778
771
  junction.ConversationDetailID = conversationDetailId;
779
772
  junction.ArtifactVersionID = version.ID;
780
- junction.Direction = 'Output'; // Artifact produced as output from task
773
+ junction.Direction = 'Output';
781
774
 
782
- const junctionSaved = await junction.Save();
783
- if (!junctionSaved) {
784
- LogError('Failed to create artifact-conversation association');
785
- return;
775
+ if (!await junction.Save()) {
776
+ throw new Error(`Failed to create artifact-conversation association: ${junction.LatestResult?.CompleteMessage ?? 'unknown error'}`);
786
777
  }
787
-
788
778
  LogStatus(`Linked artifact ${artifact.ID} to conversation detail ${conversationDetailId}`);
779
+
780
+ await provider.CommitTransaction();
789
781
  } catch (error) {
790
- LogError(`Error creating artifact from output: ${error}`);
782
+ await provider.RollbackTransaction();
783
+ LogError(`Error creating artifact from output — all changes rolled back: ${error}`);
791
784
  }
792
785
  }
793
786