@memberjunction/server 6.1.0-edge.0 → 6.1.0-edge.2

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 (175) hide show
  1. package/dist/auth/exampleNewUserSubClass.js +1 -1
  2. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  3. package/dist/auth/index.d.ts.map +1 -1
  4. package/dist/auth/index.js +9 -5
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/auth/magicLink/MagicLinkService.js +1 -1
  7. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  8. package/dist/auth/newUsers.js +1 -1
  9. package/dist/auth/newUsers.js.map +1 -1
  10. package/dist/context.js +2 -2
  11. package/dist/context.js.map +1 -1
  12. package/dist/generated/generated.d.ts +208 -646
  13. package/dist/generated/generated.d.ts.map +1 -1
  14. package/dist/generated/generated.js +1123 -3773
  15. package/dist/generated/generated.js.map +1 -1
  16. package/dist/generic/ResolverBase.d.ts +19 -3
  17. package/dist/generic/ResolverBase.d.ts.map +1 -1
  18. package/dist/generic/ResolverBase.js +51 -17
  19. package/dist/generic/ResolverBase.js.map +1 -1
  20. package/dist/index.d.ts +6 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +100 -42
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/secretRedactor.d.ts +9 -0
  25. package/dist/logging/secretRedactor.d.ts.map +1 -1
  26. package/dist/logging/secretRedactor.js +39 -0
  27. package/dist/logging/secretRedactor.js.map +1 -1
  28. package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
  29. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  30. package/dist/middleware/BaseServerMiddleware.js +5 -0
  31. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  32. package/dist/realtimeWidget/WidgetSessionService.js +1 -1
  33. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  34. package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
  35. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  36. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  37. package/dist/resolvers/ActionResolver.js +12 -0
  38. package/dist/resolvers/ActionResolver.js.map +1 -1
  39. package/dist/resolvers/ComponentRegistryResolver.js +1 -1
  40. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  41. package/dist/resolvers/EntityPermissionResolver.js +1 -1
  42. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  43. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +9 -0
  44. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  45. package/dist/resolvers/IntegrationDiscoveryResolver.js +3 -1
  46. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  47. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  48. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  49. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  50. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  51. package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
  52. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  53. package/dist/resolvers/SearchEntitiesResolver.js +1 -1
  54. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
  55. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  56. package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
  57. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  58. package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
  59. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  60. package/dist/resolvers/TaskGraphFrameResolver.d.ts +85 -0
  61. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
  62. package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
  63. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
  64. package/dist/resolvers/UserFavoriteResolver.js +1 -1
  65. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  66. package/dist/rest/OAuthCallbackHandler.js +1 -1
  67. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  68. package/dist/rest/RESTEndpointHandler.d.ts +0 -4
  69. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  70. package/dist/rest/RESTEndpointHandler.js +1 -15
  71. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  72. package/dist/rest/SignatureWebhookHandler.js +1 -1
  73. package/dist/rest/SignatureWebhookHandler.js.map +1 -1
  74. package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
  75. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
  76. package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
  77. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
  78. package/dist/services/ScheduledJobsService.js +1 -1
  79. package/dist/services/ScheduledJobsService.js.map +1 -1
  80. package/dist/services/StartTaskGraphDispatcher.d.ts +33 -0
  81. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -0
  82. package/dist/services/StartTaskGraphDispatcher.js +46 -0
  83. package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
  84. package/dist/services/TaskGraphActionRunner.d.ts +34 -0
  85. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
  86. package/dist/services/TaskGraphActionRunner.js +95 -0
  87. package/dist/services/TaskGraphActionRunner.js.map +1 -0
  88. package/dist/services/TaskGraphAgentRunner.d.ts +28 -0
  89. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
  90. package/dist/services/TaskGraphAgentRunner.js +104 -0
  91. package/dist/services/TaskGraphAgentRunner.js.map +1 -0
  92. package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
  93. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
  94. package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
  95. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
  96. package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
  97. package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
  98. package/dist/services/TaskGraphPromptRunner.js +134 -0
  99. package/dist/services/TaskGraphPromptRunner.js.map +1 -0
  100. package/dist/services/TaskGraphProviderFactory.d.ts +39 -0
  101. package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
  102. package/dist/services/TaskGraphProviderFactory.js +28 -0
  103. package/dist/services/TaskGraphProviderFactory.js.map +1 -0
  104. package/dist/telephony/RingCentralTelephonyService.js +1 -1
  105. package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
  106. package/dist/telephony/TwilioTelephonyRouter.js +1 -1
  107. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
  108. package/dist/telephony/VonageTelephonyRouter.js +1 -1
  109. package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
  110. package/package.json +92 -89
  111. package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
  112. package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
  113. package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
  114. package/src/__tests__/SessionManager.test.ts +1 -1
  115. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
  116. package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
  117. package/src/__tests__/WidgetSessionService.test.ts +1 -1
  118. package/src/__tests__/databaseAbstraction.test.ts +1 -1
  119. package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
  120. package/src/__tests__/resolverBase.rls.test.ts +3 -0
  121. package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
  122. package/src/__tests__/widgetGuestElevation.test.ts +1 -1
  123. package/src/auth/exampleNewUserSubClass.ts +1 -1
  124. package/src/auth/index.ts +10 -6
  125. package/src/auth/magicLink/MagicLinkService.ts +1 -1
  126. package/src/auth/newUsers.ts +1 -1
  127. package/src/context.ts +2 -2
  128. package/src/generated/generated.ts +955 -2735
  129. package/src/generic/ResolverBase.ts +52 -17
  130. package/src/index.ts +108 -43
  131. package/src/logging/secretRedactor.ts +43 -0
  132. package/src/middleware/BaseServerMiddleware.ts +5 -0
  133. package/src/realtimeWidget/WidgetSessionService.ts +1 -1
  134. package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
  135. package/src/resolvers/ActionResolver.ts +13 -0
  136. package/src/resolvers/ComponentRegistryResolver.ts +1 -1
  137. package/src/resolvers/EntityPermissionResolver.ts +1 -1
  138. package/src/resolvers/IntegrationDiscoveryResolver.ts +12 -2
  139. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
  140. package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
  141. package/src/resolvers/SearchEntitiesResolver.ts +1 -1
  142. package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
  143. package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
  144. package/src/resolvers/TaskGraphFrameResolver.ts +160 -0
  145. package/src/resolvers/UserFavoriteResolver.ts +1 -1
  146. package/src/rest/OAuthCallbackHandler.ts +1 -1
  147. package/src/rest/RESTEndpointHandler.ts +1 -15
  148. package/src/rest/SignatureWebhookHandler.ts +1 -1
  149. package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
  150. package/src/services/ScheduledJobsService.ts +1 -1
  151. package/src/services/StartTaskGraphDispatcher.ts +78 -0
  152. package/src/services/TaskGraphActionRunner.ts +99 -0
  153. package/src/services/TaskGraphAgentRunner.ts +115 -0
  154. package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
  155. package/src/services/TaskGraphPromptRunner.ts +144 -0
  156. package/src/services/TaskGraphProviderFactory.ts +53 -0
  157. package/src/telephony/RingCentralTelephonyService.ts +1 -1
  158. package/src/telephony/TwilioTelephonyRouter.ts +1 -1
  159. package/src/telephony/VonageTelephonyRouter.ts +1 -1
  160. package/dist/resolvers/ReportResolver.d.ts +0 -24
  161. package/dist/resolvers/ReportResolver.d.ts.map +0 -1
  162. package/dist/resolvers/ReportResolver.js +0 -198
  163. package/dist/resolvers/ReportResolver.js.map +0 -1
  164. package/dist/resolvers/TaskResolver.d.ts +0 -31
  165. package/dist/resolvers/TaskResolver.d.ts.map +0 -1
  166. package/dist/resolvers/TaskResolver.js +0 -157
  167. package/dist/resolvers/TaskResolver.js.map +0 -1
  168. package/dist/services/TaskOrchestrator.d.ts +0 -130
  169. package/dist/services/TaskOrchestrator.d.ts.map +0 -1
  170. package/dist/services/TaskOrchestrator.js +0 -692
  171. package/dist/services/TaskOrchestrator.js.map +0 -1
  172. package/src/resolvers/ReportResolver.ts +0 -165
  173. package/src/resolvers/TaskResolver.ts +0 -148
  174. package/src/resolvers/__tests__/ReportResolver.test.ts +0 -279
  175. package/src/services/TaskOrchestrator.ts +0 -873
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @fileoverview Live signal for durable task graphs — the dispatcher's lifecycle frames, published
3
+ * to anyone watching a given graph.
4
+ *
5
+ * **Why frames rather than cache invalidation.** The alternative considered was emitting
6
+ * invalidation events after each CAS claim so clients re-read Task rows. That works, but it makes
7
+ * every consumer reconstruct meaning by diffing rows, and couples the UI to claim mechanics. A
8
+ * frame says what happened — "this step failed, here is why" — so a viewer renders progress
9
+ * directly. This is §3.4's originally-promised mechanism.
10
+ *
11
+ * **Addressed by `ParentTaskID`, deliberately not by session.** A durable graph outlives the tab
12
+ * that submitted it, and may be started by a schedule with no session at all. Keying on the graph
13
+ * means "watch this workflow run" works for whoever is permitted to see it, whenever they arrive —
14
+ * including after a page refresh, which a session-keyed push cannot survive.
15
+ *
16
+ * @module @memberjunction/server
17
+ */
18
+ import { Arg, Field, ID, ObjectType, PubSubEngine, Resolver, ResolverFilterData, Root, Subscription } from 'type-graphql';
19
+ import { LogError } from '@memberjunction/core';
20
+ import { UUIDsEqual } from '@memberjunction/global';
21
+ import type { TaskGraphFrame, TaskGraphObserver } from '@memberjunction/task-graph';
22
+
23
+ export const TASK_GRAPH_FRAMES_TOPIC = 'TASK_GRAPH_FRAMES';
24
+
25
+ /** One lifecycle event, as it reaches a subscriber. */
26
+ @ObjectType()
27
+ export class TaskGraphFrameNotification {
28
+ @Field(() => String)
29
+ kind!: string;
30
+
31
+ @Field(() => ID)
32
+ parentTaskId!: string;
33
+
34
+ @Field(() => ID, { nullable: true })
35
+ taskId?: string;
36
+
37
+ @Field(() => String, { nullable: true })
38
+ taskName?: string;
39
+
40
+ @Field(() => String, { nullable: true })
41
+ status?: string;
42
+
43
+ @Field(() => String, { nullable: true })
44
+ errorMessage?: string;
45
+
46
+ @Field(() => ID, { nullable: true })
47
+ assignedUserId?: string;
48
+
49
+ @Field(() => Number, { nullable: true })
50
+ completedCount?: number;
51
+
52
+ @Field(() => Number, { nullable: true })
53
+ totalCount?: number;
54
+
55
+ @Field(() => Date)
56
+ date!: Date;
57
+ }
58
+
59
+ /**
60
+ * Wire payload. Identical to {@link TaskGraphFrame} plus the graph's owner, which exists ONLY as the
61
+ * server-side filter key and is never returned to a subscriber — the same discipline
62
+ * `PushStatusNotificationPayload` uses for `ownerUserId`.
63
+ */
64
+ export interface TaskGraphFramePayload extends TaskGraphFrame {
65
+ /** User the graph belongs to. Filter key; never sent to the client. */
66
+ ownerUserId: string;
67
+ }
68
+
69
+ interface TaskGraphFrameArgs {
70
+ parentTaskId: string;
71
+ }
72
+
73
+ /** Minimal shape of the subscription's connection context needed by the filter. */
74
+ export interface TaskGraphFrameFilterContext {
75
+ userPayload?: { userRecord?: { ID?: string } };
76
+ }
77
+
78
+ /**
79
+ * Delivery predicate. A frame reaches a subscriber only when BOTH hold:
80
+ * 1. it belongs to the graph the subscriber asked to watch, AND
81
+ * 2. the graph's owner matches the subscriber CONNECTION's authenticated identity.
82
+ *
83
+ * Condition 2 is load-bearing, and for the same reason `statusUpdatesFilter` needs it: a
84
+ * `parentTaskId` is discoverable, so without an identity check anyone holding one could watch
85
+ * another user's workflow — including its per-step error messages. **Fails closed** — a missing
86
+ * identity on either side never matches.
87
+ */
88
+ export function taskGraphFrameFilter(data: {
89
+ payload: TaskGraphFramePayload;
90
+ args: TaskGraphFrameArgs;
91
+ context: TaskGraphFrameFilterContext | undefined;
92
+ }): boolean {
93
+ const { payload, args, context } = data;
94
+ if (!payload.ParentTaskID || !args.parentTaskId) {
95
+ return false;
96
+ }
97
+ if (!UUIDsEqual(payload.ParentTaskID, args.parentTaskId)) {
98
+ return false;
99
+ }
100
+ const connectionUserId = context?.userPayload?.userRecord?.ID;
101
+ if (!connectionUserId || !payload.ownerUserId) {
102
+ return false; // fail closed
103
+ }
104
+ return UUIDsEqual(payload.ownerUserId, connectionUserId);
105
+ }
106
+
107
+ /**
108
+ * MJServer's `TaskGraphObserver` — turns dispatcher frames into a PubSub publish.
109
+ *
110
+ * `OnFrame` is synchronous and swallows everything by contract: a frame is commentary on work, never
111
+ * a step of it, so nothing here may stall or fail a graph. The publish itself is fire-and-forget for
112
+ * the same reason.
113
+ */
114
+ export class TaskGraphFrameBroadcaster implements TaskGraphObserver {
115
+ constructor(private readonly pubSub: PubSubEngine) {}
116
+
117
+ public OnFrame(frame: TaskGraphFrame): void {
118
+ try {
119
+ if (!frame.OwnerUserID) {
120
+ // A graph with no recorded owner cannot be authorized to anyone, so publishing would only
121
+ // produce frames the filter refuses. Dropping is the same outcome, without the traffic.
122
+ // Reachable for graphs submitted before ownership was recorded on the parent.
123
+ return;
124
+ }
125
+ void this.pubSub.publish(
126
+ TASK_GRAPH_FRAMES_TOPIC,
127
+ { ...frame, ownerUserId: frame.OwnerUserID } satisfies TaskGraphFramePayload,
128
+ );
129
+ } catch (e) {
130
+ LogError(`[TaskGraphFrameBroadcaster] Could not publish ${frame.Kind} (ignored): ${e instanceof Error ? e.message : String(e)}`);
131
+ }
132
+ }
133
+ }
134
+
135
+ @Resolver()
136
+ export class TaskGraphFrameResolver {
137
+ @Subscription(() => TaskGraphFrameNotification, {
138
+ topics: TASK_GRAPH_FRAMES_TOPIC,
139
+ filter: (data: ResolverFilterData<TaskGraphFramePayload, TaskGraphFrameArgs, TaskGraphFrameFilterContext>) =>
140
+ taskGraphFrameFilter(data),
141
+ })
142
+ taskGraphFrames(
143
+ @Root() payload: TaskGraphFramePayload,
144
+ @Arg('parentTaskId', () => ID) parentTaskId: string
145
+ ): TaskGraphFrameNotification {
146
+ // ownerUserId is intentionally NOT returned — it is a server-side filter key only.
147
+ return {
148
+ kind: payload.Kind,
149
+ parentTaskId,
150
+ taskId: payload.TaskID,
151
+ taskName: payload.TaskName,
152
+ status: payload.Status,
153
+ errorMessage: payload.ErrorMessage,
154
+ assignedUserId: payload.AssignedUserID,
155
+ completedCount: payload.CompletedCount,
156
+ totalCount: payload.TotalCount,
157
+ date: new Date(),
158
+ };
159
+ }
160
+ }
@@ -14,7 +14,7 @@ import {
14
14
  Query,
15
15
  Resolver,
16
16
  } from '@memberjunction/server';
17
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
17
+ import { UserCache } from '@memberjunction/generic-database-provider';
18
18
 
19
19
  import { MJUserFavorite_, MJUserFavoriteResolverBase } from '../generated/generated.js';
20
20
  import { GetReadOnlyProvider } from '../util.js';
@@ -15,7 +15,7 @@
15
15
  import express from 'express';
16
16
  import { LogError, LogStatus, RunView, UserInfo } from '@memberjunction/core';
17
17
  import { UUIDsEqual } from '@memberjunction/global';
18
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
18
+ import { UserCache } from '@memberjunction/generic-database-provider';
19
19
  import { OAuthManager, MCPClientManager } from '@memberjunction/ai-mcp-client';
20
20
  import type { MCPServerOAuthConfig } from '@memberjunction/ai-mcp-client';
21
21
 
@@ -189,8 +189,7 @@ export class RESTEndpointHandler {
189
189
  // Transaction operations
190
190
  this.router.post('/transactions', this.executeTransaction.bind(this));
191
191
 
192
- // Reports and queries
193
- this.router.get('/reports/:reportId', this.runReport.bind(this));
192
+ // Queries
194
193
  this.router.post('/queries/run', this.runQuery.bind(this));
195
194
 
196
195
  // Error handling
@@ -781,19 +780,6 @@ export class RESTEndpointHandler {
781
780
  }
782
781
  }
783
782
 
784
- /**
785
- * Run a report
786
- */
787
- private async runReport(req: express.Request, res: express.Response): Promise<void> {
788
- try {
789
- // Placeholder implementation - this would need to be implemented to run reports
790
- res.status(501).json({ error: 'Not implemented' });
791
- } catch (error) {
792
- LogError(error);
793
- res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
794
- }
795
- }
796
-
797
783
  /**
798
784
  * Run a query
799
785
  */
@@ -14,7 +14,7 @@
14
14
  import express from 'express';
15
15
  import BodyParser from 'body-parser';
16
16
  import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
17
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
17
+ import { UserCache } from '@memberjunction/generic-database-provider';
18
18
  import { SignatureEngine } from '@memberjunction/esignature/server';
19
19
 
20
20
  /**
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @fileoverview MJServer's implementation of the `DurableEntityActionSubmitter` seam.
3
+ *
4
+ * Turns one After* entity-action dispatch into a single-node durable task graph, which is D14's
5
+ * whole answer to "run this durably": not a new queue, the substrate that already has a claim
6
+ * protocol, restart recovery and orphan reclaim.
7
+ *
8
+ * Registered at boot beside the dispatcher, because the two are one capability — a submitter with
9
+ * no dispatcher writes rows nobody picks up, which is worse than staying inline.
10
+ *
11
+ * @module @memberjunction/server
12
+ */
13
+ import type {
14
+ DurableEntityActionRequest,
15
+ DurableEntityActionSubmission,
16
+ DurableEntityActionSubmitter,
17
+ } from '@memberjunction/actions-base';
18
+ import { LogError } from '@memberjunction/core';
19
+ import { TaskNode, type TaskGraphSpec } from '@memberjunction/ai-core-plus';
20
+ import { MJEnvironmentEntityExtended } from '@memberjunction/core-entities';
21
+ import { TaskGraphService, type ProviderFactory } from '@memberjunction/task-graph';
22
+
23
+ export class DurableEntityActionTaskSubmitter implements DurableEntityActionSubmitter {
24
+ /**
25
+ * @param environmentID which environment the tasks belong to. Defaults to the platform default
26
+ * because a save has no environment of its own to inherit — the record's entity is global, and
27
+ * inventing a per-save environment would put a durable dispatch somewhere nobody looks.
28
+ */
29
+ constructor(
30
+ private readonly providerFactory: ProviderFactory,
31
+ private readonly environmentID: string = MJEnvironmentEntityExtended.DefaultEnvironmentID,
32
+ ) {}
33
+
34
+ public async Submit(request: DurableEntityActionRequest): Promise<DurableEntityActionSubmission> {
35
+ try {
36
+ const spec: TaskGraphSpec = {
37
+ workflowName: `${request.ActionName} — ${request.EntityName}`,
38
+ reasoning:
39
+ `Durable dispatch of entity action binding ${request.EntityActionID} on ` +
40
+ `${request.EntityName} (${request.RecordID}), fired by ${request.InvocationType}.`,
41
+ tasks: [
42
+ TaskNode.Action(
43
+ {
44
+ tempId: 'action',
45
+ name: request.ActionName,
46
+ description: `Run ${request.ActionName} for ${request.EntityName} ${request.RecordID}.`,
47
+ dependsOn: [],
48
+ inputPayload: request.RedactedParams,
49
+ },
50
+ { actionName: request.ActionName },
51
+ ),
52
+ ],
53
+ // Nothing to say to anyone: this graph exists because a record was saved, not
54
+ // because a person asked a question. Defaulting to 'message' would post an
55
+ // orphaned result into whichever conversation happened to be nearby.
56
+ continuation: 'none',
57
+ // The point of the exercise. A one-node graph constant-folds to in-run execution by
58
+ // default (D9), which is exactly the fire-and-forget behaviour being escaped.
59
+ durable: true,
60
+ };
61
+
62
+ const provider = await this.providerFactory.CreateProvider();
63
+ const result = await new TaskGraphService().Submit(spec, {
64
+ EnvironmentID: this.environmentID,
65
+ ContextUser: request.ContextUser,
66
+ Provider: provider,
67
+ });
68
+
69
+ return result.Success
70
+ ? { Success: true, ParentTaskID: result.ParentTaskID }
71
+ : { Success: false, ErrorMessage: result.ErrorMessage };
72
+ } catch (e) {
73
+ const message = e instanceof Error ? e.message : String(e);
74
+ LogError(`[DurableEntityActionTaskSubmitter] Submission failed for binding ${request.EntityActionID}: ${message}`);
75
+ return { Success: false, ErrorMessage: message };
76
+ }
77
+ }
78
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { LogError, LogStatus, LogStatusEx, UserInfo } from '@memberjunction/core';
7
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
7
+ import { UserCache } from '@memberjunction/generic-database-provider';
8
8
  import { SchedulingEngine } from '@memberjunction/scheduling-engine';
9
9
  import { ScheduledJobsConfig } from '../config.js';
10
10
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @fileoverview Boots the durable task-graph dispatcher inside MJServer.
3
+ *
4
+ * Phase 2 landed the dispatcher and the submission path but never started anything, so a submitted
5
+ * graph persisted correctly and then sat in `Pending` forever. Submission being durable is only
6
+ * half the promise — something has to *pick the work up*.
7
+ *
8
+ * The continuation deliverer is supplied here for the same reason the agent runner is: posting into
9
+ * a conversation is a host concern the package deliberately refuses to know about.
10
+ *
11
+ * Registration of the package's Remote Operations is a separate concern handled by the generated
12
+ * class-registration manifests (`ServerBootstrap` / `ServerBootstrapLite`), which import the
13
+ * subclasses so their `@RegisterClass` decorators run. `LoadTaskGraphOperations` is called here
14
+ * anyway as a cheap guard: a host that starts a dispatcher but never loaded a manifest would accept
15
+ * submissions that route to the CodeGen contract-only base and silently do nothing.
16
+ *
17
+ * @module @memberjunction/server
18
+ */
19
+ import { UserInfo } from '@memberjunction/core';
20
+ import { LoadTaskGraphOperations, TaskGraphDispatcher } from '@memberjunction/task-graph';
21
+ import { TaskGraphPromptRunner } from './TaskGraphPromptRunner.js';
22
+ import sql from 'mssql';
23
+ import { DurableEntityActionRegistry } from '@memberjunction/actions-base';
24
+ import { CreateTaskGraphProviderFactory } from './TaskGraphProviderFactory.js';
25
+ import { TaskGraphActionRunner } from './TaskGraphActionRunner.js';
26
+ import { TaskGraphAgentRunner } from './TaskGraphAgentRunner.js';
27
+ import { DurableEntityActionTaskSubmitter } from './DurableEntityActionTaskSubmitter.js';
28
+ import { TaskGraphContinuationDeliverer } from './TaskGraphContinuationDeliverer.js';
29
+ import { PubSubManager } from '../generic/PubSubManager.js';
30
+ import { TaskGraphFrameBroadcaster } from '../resolvers/TaskGraphFrameResolver.js';
31
+
32
+ /**
33
+ * Starts one dispatcher for this process and returns it.
34
+ *
35
+ * The instance ID must be stable for the process lifetime and distinct per instance — it is what
36
+ * lets reconciliation tell "my own orphaned work, reclaim it" from "another live instance's work,
37
+ * leave it alone". `pid` plus hostname satisfies both: stable while the process lives, and never
38
+ * reused by a peer.
39
+ *
40
+ * The dispatcher self-registers with `ShutdownRegistry`, so the caller does not wire teardown.
41
+ */
42
+ export async function StartTaskGraphDispatcher(
43
+ pool: sql.ConnectionPool,
44
+ contextUser: UserInfo,
45
+ instanceID?: string,
46
+ ): Promise<TaskGraphDispatcher> {
47
+ LoadTaskGraphOperations();
48
+
49
+ const providerFactory = CreateTaskGraphProviderFactory(pool);
50
+ const pubSub = PubSubManager.Instance.PubSubEngine;
51
+ const dispatcher = new TaskGraphDispatcher(
52
+ providerFactory,
53
+ new TaskGraphAgentRunner(),
54
+ contextUser,
55
+ { InstanceID: instanceID ?? `${process.env.HOSTNAME ?? 'mjapi'}-${process.pid}` },
56
+ // Without this the dispatcher had nowhere to deliver: a finished graph logged its outcome,
57
+ // marked itself delivered, and said nothing to the conversation that asked for it.
58
+ new TaskGraphContinuationDeliverer(providerFactory, contextUser),
59
+ // Live signal. Supplied only when a PubSub engine exists — in a worker or a test there is
60
+ // nobody to publish to, and the dispatcher runs identically without one.
61
+ pubSub ? new TaskGraphFrameBroadcaster(pubSub) : undefined,
62
+ // Action nodes — the third execution shape, beside agents and people. Durable After* entity
63
+ // actions land as these.
64
+ new TaskGraphActionRunner(),
65
+ // Prompt nodes. Without this a Flow agent with a Prompt step submits and then
66
+ // stalls on a task nothing can execute — which is why the shipped User Onboarding
67
+ // Flow Agent could not run at all before this seam existed.
68
+ new TaskGraphPromptRunner(),
69
+ );
70
+ await dispatcher.Start();
71
+
72
+ // Registered only AFTER the dispatcher is running, and only here. A submitter without a
73
+ // dispatcher writes Task rows nobody picks up — strictly worse than staying inline, because the
74
+ // work is now both undone and invisible in the place it used to be logged.
75
+ DurableEntityActionRegistry.Instance.Register(new DurableEntityActionTaskSubmitter(providerFactory));
76
+
77
+ return dispatcher;
78
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @fileoverview MJServer's implementation of the task-graph `TaskActionRunner` seam.
3
+ *
4
+ * The counterpart to {@link TaskGraphAgentRunner}: the dispatcher knows *when* an action-assigned
5
+ * node should run and nothing about how an action executes, so execution is injected. Keeping it
6
+ * here rather than in `@memberjunction/task-graph` is not tidiness — the action engine depends on
7
+ * the entity layer the dispatcher also builds on, and importing it there would make every consumer
8
+ * of durable execution load the action engine as well.
9
+ *
10
+ * @module @memberjunction/server
11
+ */
12
+ import { ActionEngineServer } from '@memberjunction/actions';
13
+ import { ActionParam } from '@memberjunction/actions-base';
14
+ import { LogError } from '@memberjunction/core';
15
+ import { UUIDsEqual } from '@memberjunction/global';
16
+ import type { TaskActionRunner, TaskActionRunParams, TaskActionRunResult } from '@memberjunction/task-graph';
17
+
18
+ export class TaskGraphActionRunner implements TaskActionRunner {
19
+ public async RunActionForTask(params: TaskActionRunParams): Promise<TaskActionRunResult> {
20
+ try {
21
+ await ActionEngineServer.Instance.Config(false, params.ContextUser);
22
+ const action = ActionEngineServer.Instance.Actions.find((a) => UUIDsEqual(a.ID, params.ActionID));
23
+ if (!action) {
24
+ return { Success: false, ErrorMessage: `Action ${params.ActionID} is not in the engine's metadata.` };
25
+ }
26
+
27
+ const result = await ActionEngineServer.Instance.RunAction({
28
+ Action: action,
29
+ ContextUser: params.ContextUser,
30
+ Params: this.buildParams(params),
31
+ // No Filters. A durable node is work that a filter ALREADY let through — the entity
32
+ // action's gate ran at dispatch time, on the change context that no longer exists by
33
+ // the time the dispatcher picks the task up. Re-evaluating here would ask a
34
+ // transition question with nothing to answer it, and fail closed on every retry.
35
+ Filters: [],
36
+ });
37
+
38
+ return {
39
+ Success: result.Success,
40
+ Output: this.buildOutput(result.Params),
41
+ ErrorMessage: result.Success ? undefined : result.Message,
42
+ // The engine already wrote the log; this is the only place its id is in hand. Without
43
+ // carrying it out, a workflow's action step has no path back to its own execution
44
+ // record — the one thing anyone wants when an action misbehaves.
45
+ ActionLogID: result.LogEntry?.ID,
46
+ };
47
+ } catch (e) {
48
+ const message = e instanceof Error ? e.message : String(e);
49
+ LogError(`[TaskGraphActionRunner] Task ${params.TaskID} failed: ${message}`);
50
+ return { Success: false, ErrorMessage: message };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Projects the action's parameters onto the flat result a workflow step is expected to produce.
56
+ *
57
+ * **Why not return `result.Params` as-is.** That is an `ActionParam[]`, and every consumer of a
58
+ * step's output addresses fields BY NAME: an output mapping says `{"CurrentPrice": "stockPrice"}`,
59
+ * and a branch condition then reads `payload.stockPrice`. Handed an array, the mapping finds no
60
+ * `CurrentPrice`, writes nothing, and the condition evaluates against `undefined` — which is
61
+ * falsy rather than erroneous, so the workflow takes the other branch and reports success. That
62
+ * is exactly what the Demo workflow did: step 1 completed with an output payload of `{}` and
63
+ * every downstream branch was skipped, with nothing anywhere reporting a problem.
64
+ *
65
+ * Only `Output` and `Both` params are included. Echoing the inputs back would let a step's own
66
+ * parameters masquerade as its results, so an output mapping could "succeed" by reading the
67
+ * value the step was given rather than the one it produced.
68
+ */
69
+ private buildOutput(actionParams: ActionParam[]): Record<string, unknown> {
70
+ const output: Record<string, unknown> = {};
71
+ for (const p of actionParams ?? []) {
72
+ if (p.Type === 'Output' || p.Type === 'Both') output[p.Name] = p.Value;
73
+ }
74
+ return output;
75
+ }
76
+
77
+ /**
78
+ * Rebuilds the action's parameters from the task's stored payload.
79
+ *
80
+ * **The values here have been through redaction**, because `Task.InputPayload` is persistent,
81
+ * user-visible storage and nothing writes a raw `ActionParam[]` there. A parameter the binding
82
+ * marked as not-logged therefore arrives absent rather than secret — the action sees a missing
83
+ * value, which is the honest consequence of choosing not to persist it, and the reason durable
84
+ * dispatch is opt-in per binding rather than the default.
85
+ *
86
+ * Dependency outputs are merged underneath the task's own input so a node's explicit parameters
87
+ * always win over an upstream node that happened to emit the same key.
88
+ */
89
+ private buildParams(params: TaskActionRunParams): ActionParam[] {
90
+ const merged: Record<string, unknown> = {};
91
+ for (const [name, value] of params.DependencyOutputs) {
92
+ merged[name] = value;
93
+ }
94
+ if (params.InputPayload && typeof params.InputPayload === 'object') {
95
+ Object.assign(merged, params.InputPayload as Record<string, unknown>);
96
+ }
97
+ return Object.entries(merged).map(([Name, Value]) => ({ Name, Value, Type: 'Input' }));
98
+ }
99
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview MJServer's implementation of the task-graph `TaskAgentRunner` seam.
3
+ *
4
+ * The dispatcher knows *when* a task should run and *what* it depends on, but nothing about how an
5
+ * agent executes. That is injected, so the package stays testable without standing up the agent
6
+ * framework and so a host with a different execution strategy (a queue, a remote worker) can
7
+ * substitute its own.
8
+ *
9
+ * This adapter is the thin translation between the two: it turns a task's input payload and its
10
+ * dependencies' outputs into agent messages, runs the agent, and maps the result back into the
11
+ * shape the dispatcher persists.
12
+ *
13
+ * @module @memberjunction/server
14
+ */
15
+ import { AgentRunner } from '@memberjunction/ai-agents';
16
+ import { ChatMessageRole } from '@memberjunction/ai';
17
+ import { LogError } from '@memberjunction/core';
18
+ import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended } from '@memberjunction/ai-core-plus';
19
+ import type { TaskAgentRunner, TaskAgentRunParams, TaskAgentRunResult } from '@memberjunction/task-graph';
20
+
21
+ export class TaskGraphAgentRunner implements TaskAgentRunner {
22
+ public async RunAgentForTask(params: TaskAgentRunParams): Promise<TaskAgentRunResult> {
23
+ try {
24
+ const agent = await params.Provider.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', params.ContextUser);
25
+ if (!(await agent.Load(params.AgentID))) {
26
+ return { Success: false, ErrorMessage: `Agent ${params.AgentID} could not be loaded.` };
27
+ }
28
+
29
+ const result = await new AgentRunner().RunAgent({
30
+ agent,
31
+ conversationMessages: [{ role: ChatMessageRole.user, content: this.buildPrompt(params) }],
32
+ contextUser: params.ContextUser,
33
+ // Inherited from the graph, so a graph this run goes on to emit is one hop deeper
34
+ // rather than starting the chain over. Without it MAX_REINVOKE_DEPTH never fires.
35
+ continuationDepth: params.ContinuationDepth ?? 0,
36
+ parentRun: await this.loadSubmittingRun(params),
37
+ });
38
+
39
+ const success = result?.success === true;
40
+ return {
41
+ Success: success,
42
+ Output: this.extractOutput(result),
43
+ ErrorMessage: success ? undefined : (result?.agentRun?.ErrorMessage ?? 'Agent execution failed'),
44
+ AgentRunID: result?.agentRun?.ID,
45
+ };
46
+ } catch (e) {
47
+ const message = e instanceof Error ? e.message : String(e);
48
+ LogError(`[TaskGraphAgentRunner] Task ${params.TaskID} failed: ${message}`);
49
+ return { Success: false, ErrorMessage: message };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * The run that submitted this graph, as the entity `RunAgent` wants for `parentRun`.
55
+ *
56
+ * **Why this link is worth a load.** It sets `ParentRunID` on the spawned run, which turns a
57
+ * workflow's total cost into one indexed sum over `RootParentRunID` rather than a walk of the
58
+ * graph, and gives the run a "started by" trail someone can follow back. Without it every
59
+ * dispatched run looks like it began from nowhere.
60
+ *
61
+ * Undefined — never a throw — when there is no submitting run or it cannot be read. A graph
62
+ * submitted by a schedule, by MCP, or by a person genuinely has no parent run, and failing the
63
+ * step because its provenance is unavailable would trade real work for a bookkeeping detail.
64
+ */
65
+ private async loadSubmittingRun(params: TaskAgentRunParams): Promise<MJAIAgentRunEntityExtended | undefined> {
66
+ if (!params.SubmittingAgentRunID) return undefined;
67
+ try {
68
+ const run = await params.Provider.GetEntityObject<MJAIAgentRunEntityExtended>(
69
+ 'MJ: AI Agent Runs',
70
+ params.ContextUser,
71
+ );
72
+ return (await run.Load(params.SubmittingAgentRunID)) ? run : undefined;
73
+ } catch (e) {
74
+ LogError(
75
+ `[TaskGraphAgentRunner] Task ${params.TaskID}: submitting run ` +
76
+ `${params.SubmittingAgentRunID} could not be loaded; the spawned run will have no ` +
77
+ `parent link. ${e instanceof Error ? e.message : String(e)}`,
78
+ );
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Builds the agent's instruction from the task's own input plus its dependencies' outputs.
85
+ *
86
+ * Dependency outputs are included inline because a task's whole reason for depending on another
87
+ * is to consume what it produced. They are rendered as labelled JSON rather than prose so the
88
+ * agent can parse them reliably rather than inferring structure from formatting.
89
+ */
90
+ private buildPrompt(params: TaskAgentRunParams): string {
91
+ const sections: string[] = [];
92
+
93
+ if (params.InputPayload != null) {
94
+ sections.push(`## Task input\n\`\`\`json\n${JSON.stringify(params.InputPayload, null, 2)}\n\`\`\``);
95
+ }
96
+
97
+ if (params.DependencyOutputs.size > 0) {
98
+ const rendered = [...params.DependencyOutputs.entries()]
99
+ .map(([taskID, output]) => `### Output of task ${taskID}\n\`\`\`json\n${JSON.stringify(output, null, 2)}\n\`\`\``)
100
+ .join('\n\n');
101
+ sections.push(`## Results from prerequisite tasks\n\n${rendered}`);
102
+ }
103
+
104
+ return sections.length > 0
105
+ ? sections.join('\n\n')
106
+ : 'Execute this task.';
107
+ }
108
+
109
+ /** Prefers a structured payload over prose, so downstream tasks get data rather than text. */
110
+ private extractOutput(result: unknown): unknown {
111
+ const r = result as { payload?: unknown; agentRun?: { Message?: string } } | null;
112
+ if (r?.payload != null) return r.payload;
113
+ return r?.agentRun?.Message ?? null;
114
+ }
115
+ }