@memberjunction/server 5.51.0 → 6.1.0-edge.1

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 (147) hide show
  1. package/LICENSE +7 -0
  2. package/dist/agentSessions/SessionManager.d.ts +22 -1
  3. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  4. package/dist/agentSessions/SessionManager.js +13 -3
  5. package/dist/agentSessions/SessionManager.js.map +1 -1
  6. package/dist/auth/actingContextResolver.d.ts +44 -0
  7. package/dist/auth/actingContextResolver.d.ts.map +1 -0
  8. package/dist/auth/actingContextResolver.js +18 -0
  9. package/dist/auth/actingContextResolver.js.map +1 -0
  10. package/dist/auth/index.d.ts +1 -0
  11. package/dist/auth/index.d.ts.map +1 -1
  12. package/dist/auth/index.js +1 -0
  13. package/dist/auth/index.js.map +1 -1
  14. package/dist/auth/sessionUserClone.d.ts +32 -0
  15. package/dist/auth/sessionUserClone.d.ts.map +1 -0
  16. package/dist/auth/sessionUserClone.js +50 -0
  17. package/dist/auth/sessionUserClone.js.map +1 -0
  18. package/dist/context.d.ts +1 -1
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +43 -6
  21. package/dist/context.js.map +1 -1
  22. package/dist/generated/generated.d.ts +234 -642
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +1277 -3755
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
  27. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  28. package/dist/generic/FireAndForgetHeartbeat.js +5 -4
  29. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  30. package/dist/generic/PushStatusResolver.d.ts +58 -0
  31. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  32. package/dist/generic/PushStatusResolver.js +42 -3
  33. package/dist/generic/PushStatusResolver.js.map +1 -1
  34. package/dist/generic/ResolverBase.d.ts +9 -0
  35. package/dist/generic/ResolverBase.d.ts.map +1 -1
  36. package/dist/generic/ResolverBase.js +31 -13
  37. package/dist/generic/ResolverBase.js.map +1 -1
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +20 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/multiTenancy/index.d.ts +19 -0
  43. package/dist/multiTenancy/index.d.ts.map +1 -1
  44. package/dist/multiTenancy/index.js +82 -5
  45. package/dist/multiTenancy/index.js.map +1 -1
  46. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  47. package/dist/resolvers/ActionResolver.js +12 -5
  48. package/dist/resolvers/ActionResolver.js.map +1 -1
  49. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  50. package/dist/resolvers/MCPResolver.js +13 -16
  51. package/dist/resolvers/MCPResolver.js.map +1 -1
  52. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -1
  53. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  54. package/dist/resolvers/RealtimeClientSessionResolver.js +15 -14
  55. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  56. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  57. package/dist/resolvers/RemoteBrowserActionResolver.js +19 -26
  58. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  59. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  60. package/dist/resolvers/RunAIAgentResolver.js +30 -40
  61. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  62. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  63. package/dist/resolvers/RunTestResolver.js +90 -112
  64. package/dist/resolvers/RunTestResolver.js.map +1 -1
  65. package/dist/resolvers/SearchKnowledgeResolver.js +3 -3
  66. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  67. package/dist/resolvers/SearchKnowledgeStreamResolver.js +2 -2
  68. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  69. package/dist/resolvers/TaskGraphFrameResolver.d.ts +85 -0
  70. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
  71. package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
  72. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
  73. package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
  74. package/dist/resolvers/VersionHistoryResolver.js +8 -12
  75. package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
  76. package/dist/rest/RESTEndpointHandler.d.ts +0 -4
  77. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  78. package/dist/rest/RESTEndpointHandler.js +1 -15
  79. package/dist/rest/RESTEndpointHandler.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 +31 -0
  83. package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
  84. package/dist/services/TaskGraphAgentRunner.d.ts +15 -0
  85. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
  86. package/dist/services/TaskGraphAgentRunner.js +74 -0
  87. package/dist/services/TaskGraphAgentRunner.js.map +1 -0
  88. package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
  89. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
  90. package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
  91. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
  92. package/dist/services/TaskGraphProviderFactory.d.ts +39 -0
  93. package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
  94. package/dist/services/TaskGraphProviderFactory.js +28 -0
  95. package/dist/services/TaskGraphProviderFactory.js.map +1 -0
  96. package/package.json +103 -100
  97. package/src/__tests__/RealtimeBridgeResolver.test.ts +6 -0
  98. package/src/__tests__/SessionManager.test.ts +44 -0
  99. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
  100. package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
  101. package/src/__tests__/multiTenancy.security.test.ts +254 -22
  102. package/src/__tests__/multiTenancy.test.ts +22 -0
  103. package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
  104. package/src/__tests__/resolverBase.rls.test.ts +4 -0
  105. package/src/__tests__/search-knowledge-tags.test.ts +74 -6
  106. package/src/__tests__/sessionUserClone.test.ts +161 -0
  107. package/src/agentSessions/SessionManager.ts +26 -2
  108. package/src/auth/actingContextResolver.ts +56 -0
  109. package/src/auth/index.ts +1 -0
  110. package/src/auth/sessionUserClone.ts +50 -0
  111. package/src/context.ts +48 -6
  112. package/src/generated/generated.ts +899 -2555
  113. package/src/generic/FireAndForgetHeartbeat.ts +10 -4
  114. package/src/generic/PushStatusResolver.ts +79 -4
  115. package/src/generic/ResolverBase.ts +33 -13
  116. package/src/index.ts +21 -4
  117. package/src/multiTenancy/index.ts +92 -5
  118. package/src/resolvers/ActionResolver.ts +15 -7
  119. package/src/resolvers/MCPResolver.ts +13 -15
  120. package/src/resolvers/RealtimeClientSessionResolver.ts +15 -13
  121. package/src/resolvers/RemoteBrowserActionResolver.ts +19 -25
  122. package/src/resolvers/RunAIAgentResolver.ts +30 -40
  123. package/src/resolvers/RunTestResolver.ts +90 -112
  124. package/src/resolvers/SearchKnowledgeResolver.ts +3 -3
  125. package/src/resolvers/SearchKnowledgeStreamResolver.ts +2 -2
  126. package/src/resolvers/TaskGraphFrameResolver.ts +160 -0
  127. package/src/resolvers/VersionHistoryResolver.ts +10 -13
  128. package/src/rest/RESTEndpointHandler.ts +1 -15
  129. package/src/services/StartTaskGraphDispatcher.ts +61 -0
  130. package/src/services/TaskGraphAgentRunner.ts +81 -0
  131. package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
  132. package/src/services/TaskGraphProviderFactory.ts +53 -0
  133. package/dist/resolvers/ReportResolver.d.ts +0 -24
  134. package/dist/resolvers/ReportResolver.d.ts.map +0 -1
  135. package/dist/resolvers/ReportResolver.js +0 -190
  136. package/dist/resolvers/ReportResolver.js.map +0 -1
  137. package/dist/resolvers/TaskResolver.d.ts +0 -31
  138. package/dist/resolvers/TaskResolver.d.ts.map +0 -1
  139. package/dist/resolvers/TaskResolver.js +0 -157
  140. package/dist/resolvers/TaskResolver.js.map +0 -1
  141. package/dist/services/TaskOrchestrator.d.ts +0 -130
  142. package/dist/services/TaskOrchestrator.d.ts.map +0 -1
  143. package/dist/services/TaskOrchestrator.js +0 -690
  144. package/dist/services/TaskOrchestrator.js.map +0 -1
  145. package/src/resolvers/ReportResolver.ts +0 -157
  146. package/src/resolvers/TaskResolver.ts +0 -148
  147. 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
+ }
@@ -1,11 +1,10 @@
1
1
  import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Int, InputType, PubSub, PubSubEngine } from 'type-graphql';
2
- import { AppContext } from '../types.js';
2
+ import { AppContext, UserPayload } from '../types.js';
3
3
  import { LogError, LogStatus, CompositeKey, KeyValuePair } from '@memberjunction/core';
4
4
  import { ResolverBase } from '../generic/ResolverBase.js';
5
5
  import { VersionHistoryEngine } from '@memberjunction/version-history';
6
6
  import { CreateLabelParams, CreateLabelProgressUpdate, VersionLabelScope } from '@memberjunction/version-history';
7
7
  import { KeyValuePairInput } from '../generic/KeyValuePairInput.js';
8
- import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
9
8
 
10
9
  // =========================================================================
11
10
  // GraphQL types
@@ -115,7 +114,7 @@ export class VersionHistoryResolver extends ResolverBase {
115
114
 
116
115
  // Build progress callback that publishes to PubSub
117
116
  const resolvedSessionId = sessionId ?? context.userPayload.sessionId;
118
- const onProgress = this.buildProgressCallback(pubSub, resolvedSessionId);
117
+ const onProgress = this.buildProgressCallback(pubSub, resolvedSessionId, context.userPayload);
119
118
 
120
119
  const params: CreateLabelParams = {
121
120
  Name: input.Name,
@@ -160,18 +159,16 @@ export class VersionHistoryResolver extends ResolverBase {
160
159
  */
161
160
  private buildProgressCallback(
162
161
  pubSub: PubSubEngine,
163
- sessionId: string
162
+ sessionId: string,
163
+ userPayload: UserPayload
164
164
  ): (progress: CreateLabelProgressUpdate) => void {
165
165
  return (progress: CreateLabelProgressUpdate) => {
166
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
167
- message: JSON.stringify({
168
- resolver: 'VersionHistoryResolver',
169
- type: 'CreateLabelProgress',
170
- status: 'ok',
171
- data: progress,
172
- }),
173
- sessionId,
174
- });
166
+ this.PublishStatusUpdate(pubSub, sessionId, JSON.stringify({
167
+ resolver: 'VersionHistoryResolver',
168
+ type: 'CreateLabelProgress',
169
+ status: 'ok',
170
+ data: progress,
171
+ }), userPayload);
175
172
  };
176
173
  }
177
174
  }
@@ -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
  */
@@ -0,0 +1,61 @@
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 sql from 'mssql';
22
+ import { CreateTaskGraphProviderFactory } from './TaskGraphProviderFactory.js';
23
+ import { TaskGraphAgentRunner } from './TaskGraphAgentRunner.js';
24
+ import { TaskGraphContinuationDeliverer } from './TaskGraphContinuationDeliverer.js';
25
+ import { PubSubManager } from '../generic/PubSubManager.js';
26
+ import { TaskGraphFrameBroadcaster } from '../resolvers/TaskGraphFrameResolver.js';
27
+
28
+ /**
29
+ * Starts one dispatcher for this process and returns it.
30
+ *
31
+ * The instance ID must be stable for the process lifetime and distinct per instance — it is what
32
+ * lets reconciliation tell "my own orphaned work, reclaim it" from "another live instance's work,
33
+ * leave it alone". `pid` plus hostname satisfies both: stable while the process lives, and never
34
+ * reused by a peer.
35
+ *
36
+ * The dispatcher self-registers with `ShutdownRegistry`, so the caller does not wire teardown.
37
+ */
38
+ export async function StartTaskGraphDispatcher(
39
+ pool: sql.ConnectionPool,
40
+ contextUser: UserInfo,
41
+ instanceID?: string,
42
+ ): Promise<TaskGraphDispatcher> {
43
+ LoadTaskGraphOperations();
44
+
45
+ const providerFactory = CreateTaskGraphProviderFactory(pool);
46
+ const pubSub = PubSubManager.Instance.PubSubEngine;
47
+ const dispatcher = new TaskGraphDispatcher(
48
+ providerFactory,
49
+ new TaskGraphAgentRunner(),
50
+ contextUser,
51
+ { InstanceID: instanceID ?? `${process.env.HOSTNAME ?? 'mjapi'}-${process.pid}` },
52
+ // Without this the dispatcher had nowhere to deliver: a finished graph logged its outcome,
53
+ // marked itself delivered, and said nothing to the conversation that asked for it.
54
+ new TaskGraphContinuationDeliverer(providerFactory, contextUser),
55
+ // Live signal. Supplied only when a PubSub engine exists — in a worker or a test there is
56
+ // nobody to publish to, and the dispatcher runs identically without one.
57
+ pubSub ? new TaskGraphFrameBroadcaster(pubSub) : undefined,
58
+ );
59
+ await dispatcher.Start();
60
+ return dispatcher;
61
+ }
@@ -0,0 +1,81 @@
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 } 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
+ });
34
+
35
+ const success = result?.success === true;
36
+ return {
37
+ Success: success,
38
+ Output: this.extractOutput(result),
39
+ ErrorMessage: success ? undefined : (result?.agentRun?.ErrorMessage ?? 'Agent execution failed'),
40
+ AgentRunID: result?.agentRun?.ID,
41
+ };
42
+ } catch (e) {
43
+ const message = e instanceof Error ? e.message : String(e);
44
+ LogError(`[TaskGraphAgentRunner] Task ${params.TaskID} failed: ${message}`);
45
+ return { Success: false, ErrorMessage: message };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Builds the agent's instruction from the task's own input plus its dependencies' outputs.
51
+ *
52
+ * Dependency outputs are included inline because a task's whole reason for depending on another
53
+ * is to consume what it produced. They are rendered as labelled JSON rather than prose so the
54
+ * agent can parse them reliably rather than inferring structure from formatting.
55
+ */
56
+ private buildPrompt(params: TaskAgentRunParams): string {
57
+ const sections: string[] = [];
58
+
59
+ if (params.InputPayload != null) {
60
+ sections.push(`## Task input\n\`\`\`json\n${JSON.stringify(params.InputPayload, null, 2)}\n\`\`\``);
61
+ }
62
+
63
+ if (params.DependencyOutputs.size > 0) {
64
+ const rendered = [...params.DependencyOutputs.entries()]
65
+ .map(([taskID, output]) => `### Output of task ${taskID}\n\`\`\`json\n${JSON.stringify(output, null, 2)}\n\`\`\``)
66
+ .join('\n\n');
67
+ sections.push(`## Results from prerequisite tasks\n\n${rendered}`);
68
+ }
69
+
70
+ return sections.length > 0
71
+ ? sections.join('\n\n')
72
+ : 'Execute this task.';
73
+ }
74
+
75
+ /** Prefers a structured payload over prose, so downstream tasks get data rather than text. */
76
+ private extractOutput(result: unknown): unknown {
77
+ const r = result as { payload?: unknown; agentRun?: { Message?: string } } | null;
78
+ if (r?.payload != null) return r.payload;
79
+ return r?.agentRun?.Message ?? null;
80
+ }
81
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @fileoverview MJServer's implementation of the task-graph `TaskContinuationDeliverer` seam.
3
+ *
4
+ * **The dispatcher shipped with nowhere to deliver.** `StartTaskGraphDispatcher` constructed it
5
+ * without a deliverer at all, so a graph that finished logged its outcome, marked itself delivered,
6
+ * and said nothing to the conversation that asked for it. Durable execution nobody hears about is
7
+ * half a promise — the same shape as Phase 2's dispatcher that ran nothing.
8
+ *
9
+ * Posting into a conversation is a host concern by design: the task-graph package must not depend on
10
+ * the conversation layer, and cannot depend on the agent framework at all without a cycle (agents
11
+ * submit graphs). This adapter is where those dependencies are allowed to live.
12
+ *
13
+ * @module @memberjunction/server
14
+ */
15
+ import { LogError, LogStatus, type IMetadataProvider, type UserInfo } from '@memberjunction/core';
16
+ import { MJAIAgentRunEntity, MJConversationDetailEntity } from '@memberjunction/core-entities';
17
+ import { MJAIAgentEntityExtended } from '@memberjunction/ai-core-plus';
18
+ import { AgentRunner } from '@memberjunction/ai-agents';
19
+ import { ChatMessageRole } from '@memberjunction/ai';
20
+ import type { ProviderFactory, TaskContinuationDeliverer, TaskContinuationParams } from '@memberjunction/task-graph';
21
+
22
+ /** How many per-task lines a posted summary shows before collapsing the rest into a count. */
23
+ const MAX_LISTED_TASKS = 20;
24
+
25
+ /**
26
+ * Delivers a finished graph's outcome — as a conversation message, or by starting the submitting
27
+ * agent a fresh turn.
28
+ *
29
+ * **The chain is bounded, and that is what took a schema change to make true.** A graph may declare
30
+ * `continuation: 'reinvoke'`, and the agent it restarts can emit another graph, which can reinvoke
31
+ * again. The guard for that (`MAX_REINVOKE_DEPTH`) shipped in Phase 3, but the number it compared
32
+ * was permanently zero: `TaskGraphService.Submit` reads `ReinvokeDepth` from its caller, and a
33
+ * reinvoked agent had no way to know it *was* a continuation. `AIAgentRun.ContinuationDepth` closes
34
+ * that loop — this stamps depth + 1 on the run it starts, `BaseAgent` passes it into any graph that
35
+ * run submits, and the cap finally compares against something real.
36
+ */
37
+ export class TaskGraphContinuationDeliverer implements TaskContinuationDeliverer {
38
+ /**
39
+ * @param providerFactory mints a fresh provider per delivery, for the same reason the dispatcher
40
+ * does: deliveries run outside any request and concurrently with task execution, so
41
+ * sharing one provider would share one transaction scope across unrelated work.
42
+ */
43
+ constructor(
44
+ private readonly providerFactory: ProviderFactory,
45
+ private readonly contextUser: UserInfo,
46
+ ) {}
47
+
48
+ /**
49
+ * Posts the roll-up as an AI-role message in the graph's conversation.
50
+ *
51
+ * Never throws. The dispatcher calls this inside the compare-and-swap that marks a completion
52
+ * delivered; an error escaping would either abort that guard or leave the graph looking
53
+ * undelivered and re-notifying on every later sweep.
54
+ */
55
+ public async PostMessage(params: TaskContinuationParams): Promise<void> {
56
+ try {
57
+ if (!params.ConversationDetailID) {
58
+ // A graph submitted headlessly — a schedule, an entity-change trigger, an API call —
59
+ // has no conversation to answer. Not an error; most workflows are in this shape.
60
+ LogStatus(`[TaskGraphContinuationDeliverer] "${params.WorkflowName}" finished with no conversation to post to.`);
61
+ return;
62
+ }
63
+
64
+ const provider = await this.providerFactory.CreateProvider();
65
+ const conversationID = await this.resolveConversationID(params.ConversationDetailID, provider);
66
+ if (!conversationID) {
67
+ LogError(
68
+ `[TaskGraphContinuationDeliverer] Conversation detail ${params.ConversationDetailID} could not be loaded — ` +
69
+ `"${params.WorkflowName}" has nowhere to post its outcome.`
70
+ );
71
+ return;
72
+ }
73
+
74
+ const detail = await provider.GetEntityObject<MJConversationDetailEntity>('MJ: Conversation Details', this.contextUser);
75
+ detail.NewRecord();
76
+ detail.ConversationID = conversationID;
77
+ detail.Role = 'AI';
78
+ detail.Status = 'Complete';
79
+ detail.HiddenToUser = false;
80
+ detail.Message = this.renderMessage(params);
81
+
82
+ if (!(await detail.Save())) {
83
+ LogError(
84
+ `[TaskGraphContinuationDeliverer] Could not post the outcome of "${params.WorkflowName}": ` +
85
+ `${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`
86
+ );
87
+ }
88
+ } catch (e) {
89
+ LogError(`[TaskGraphContinuationDeliverer] Posting the outcome of "${params.WorkflowName}" threw`, undefined, e);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Starts the submitting agent a fresh turn carrying the graph's outcome.
95
+ *
96
+ * Never throws, for the same reason `PostMessage` does not: the dispatcher calls this inside the
97
+ * compare-and-swap that marks a completion delivered.
98
+ *
99
+ * The new run records `ReinvokeDepth + 1` so the chain is bounded. The dispatcher has already
100
+ * refused to call this when the cap is reached — it degrades to `PostMessage` — so arriving here
101
+ * means there is budget left, and stamping the depth is what keeps that true for the next hop.
102
+ */
103
+ public async Reinvoke(params: TaskContinuationParams): Promise<void> {
104
+ try {
105
+ if (!params.SubmittedByAgentRunID) {
106
+ // Nothing to restart. A graph with no submitting run came from a schedule or a
107
+ // trigger, where "continue the conversation" has no meaning.
108
+ LogStatus(`[TaskGraphContinuationDeliverer] "${params.WorkflowName}" asked to reinvoke but records no submitting agent run — posting instead.`);
109
+ await this.PostMessage(params);
110
+ return;
111
+ }
112
+
113
+ const provider = await this.providerFactory.CreateProvider();
114
+ const priorRun = await provider.GetEntityObject<MJAIAgentRunEntity>('MJ: AI Agent Runs', this.contextUser);
115
+ if (!(await priorRun.Load(params.SubmittedByAgentRunID))) {
116
+ LogError(`[TaskGraphContinuationDeliverer] Submitting run ${params.SubmittedByAgentRunID} could not be loaded — posting "${params.WorkflowName}" instead.`);
117
+ await this.PostMessage(params);
118
+ return;
119
+ }
120
+
121
+ const agent = await provider.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', this.contextUser);
122
+ if (!(await agent.Load(priorRun.AgentID))) {
123
+ LogError(`[TaskGraphContinuationDeliverer] Agent ${priorRun.AgentID} could not be loaded — posting "${params.WorkflowName}" instead.`);
124
+ await this.PostMessage(params);
125
+ return;
126
+ }
127
+
128
+ await new AgentRunner().RunAgent({
129
+ agent,
130
+ conversationMessages: [{ role: ChatMessageRole.user, content: this.renderMessage(params) }],
131
+ contextUser: this.contextUser,
132
+ conversationDetailId: params.ConversationDetailID ?? undefined,
133
+ // The load-bearing value: without it the next graph this run submits restarts the
134
+ // chain at zero and the cap never fires.
135
+ continuationDepth: params.ReinvokeDepth + 1,
136
+ });
137
+ } catch (e) {
138
+ LogError(`[TaskGraphContinuationDeliverer] Reinvoking for "${params.WorkflowName}" threw`, undefined, e);
139
+ }
140
+ }
141
+
142
+ /** The conversation a detail belongs to. */
143
+ private async resolveConversationID(conversationDetailID: string, provider: IMetadataProvider): Promise<string | null> {
144
+ const detail = await provider.GetEntityObject<MJConversationDetailEntity>('MJ: Conversation Details', this.contextUser);
145
+ return (await detail.Load(conversationDetailID)) ? detail.ConversationID : null;
146
+ }
147
+
148
+ /**
149
+ * Renders the outcome as Markdown.
150
+ *
151
+ * Per-task lines rather than one aggregate status: a graph where nine of ten steps succeeded is a
152
+ * materially different message from one that failed outright, and the roll-up alone cannot say
153
+ * which step went wrong. Long graphs truncate so a fifty-step workflow does not bury the
154
+ * conversation — the task rows remain the complete record.
155
+ */
156
+ private renderMessage(params: TaskContinuationParams): string {
157
+ const lines = [`**${params.WorkflowName}** finished.`, '', params.Summary];
158
+
159
+ if (params.Tasks.length > 0) {
160
+ lines.push('');
161
+ for (const task of params.Tasks.slice(0, MAX_LISTED_TASKS)) {
162
+ const detail = task.ErrorMessage ?? task.Summary;
163
+ lines.push(`- ${this.statusIcon(task.Status)} **${task.Name}** — ${task.Status}${detail ? `: ${detail}` : ''}`);
164
+ }
165
+ if (params.Tasks.length > MAX_LISTED_TASKS) {
166
+ lines.push(`- …and ${params.Tasks.length - MAX_LISTED_TASKS} more`);
167
+ }
168
+ }
169
+
170
+ return lines.join('\n');
171
+ }
172
+
173
+ private statusIcon(status: string): string {
174
+ switch (status) {
175
+ case 'Complete': return '✅';
176
+ case 'Failed': return '❌';
177
+ case 'Cancelled': return '⊘';
178
+ default: return '•';
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview MJServer's implementation of the task-graph `ProviderFactory` seam.
3
+ *
4
+ * The durable dispatcher runs outside any HTTP request and executes tasks concurrently, so it must
5
+ * never share one provider — and therefore one transaction scope and one set of entity instances —
6
+ * across parallel work. It needs a way to mint providers on demand.
7
+ *
8
+ * Rather than have `@memberjunction/task-graph` import MJServer to get at that machinery (which
9
+ * would invert the dependency: MJServer depends on task-graph, not the reverse), the package
10
+ * declares a `ProviderFactory` interface and the host supplies an implementation. This is that
11
+ * implementation.
12
+ *
13
+ * It mints exactly what the per-request path already mints — a fresh provider over the **shared
14
+ * connection pool**, with metadata reuse — which is proven cheap at request scale and is why the
15
+ * plan chose this mechanism rather than inventing a dispatcher-specific one. The pool remains the
16
+ * real concurrency governor; the dispatcher's own `MaxConcurrentTasks` sits below it.
17
+ *
18
+ * @module @memberjunction/server
19
+ */
20
+ import { DatabaseProviderBase, IMetadataProvider } from '@memberjunction/core';
21
+ import type { ProviderFactory } from '@memberjunction/task-graph';
22
+ import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
23
+ import sql from 'mssql';
24
+ import { mj_core_schema } from '../config.js';
25
+
26
+ /**
27
+ * Creates providers for the dispatcher, one per task execution.
28
+ *
29
+ * SQL Server path only for now, matching where the dispatcher is hosted. The PostgreSQL branch of
30
+ * `createPerRequestProviders` is deliberately not duplicated here — when PG hosting is needed, the
31
+ * right move is to lift the shared branch out of `context.ts` rather than fork it a second time.
32
+ */
33
+ export class TaskGraphProviderFactory implements ProviderFactory {
34
+ constructor(private readonly pool: sql.ConnectionPool) {}
35
+
36
+ public async CreateProvider(): Promise<IMetadataProvider> {
37
+ // `loadIfNeeded = false` (the trailing false) reuses already-loaded metadata rather than
38
+ // re-reading it per provider — the difference between "cheap enough to do per task" and
39
+ // "prohibitively expensive".
40
+ const config = new SQLServerProviderConfigData(this.pool, mj_core_schema, 0, undefined, undefined, false);
41
+ const provider = new SQLServerDataProvider();
42
+ await provider.Config(config);
43
+ return provider as unknown as IMetadataProvider;
44
+ }
45
+ }
46
+
47
+ /** Convenience for hosts that already hold a pool. */
48
+ export function CreateTaskGraphProviderFactory(pool: sql.ConnectionPool): ProviderFactory {
49
+ return new TaskGraphProviderFactory(pool);
50
+ }
51
+
52
+ /** Re-exported for hosts that need the concrete type. */
53
+ export type { DatabaseProviderBase };
@@ -1,24 +0,0 @@
1
- import { AppContext } from '../types.js';
2
- import { ResolverBase } from '../generic/ResolverBase.js';
3
- export declare class RunReportResultType {
4
- ReportID: string;
5
- Success: boolean;
6
- Results: string;
7
- RowCount: number;
8
- ExecutionTime: number;
9
- ErrorMessage: string;
10
- }
11
- export declare class CreateReportResultType {
12
- ReportID: string;
13
- ReportName: string;
14
- Success: boolean;
15
- ErrorMessage: string;
16
- }
17
- export declare class ReportResolverExtended extends ResolverBase {
18
- GetReportData(ReportID: string, context: AppContext): Promise<RunReportResultType>;
19
- /**
20
- * This mutation will create a new report from a conversation detail ID
21
- */
22
- CreateReportFromConversationDetailID(ConversationDetailID: string, { dataSource, userPayload, providers }: AppContext): Promise<CreateReportResultType>;
23
- }
24
- //# sourceMappingURL=ReportResolver.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ReportResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/ReportResolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAOzC,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,qBACa,mBAAmB;IAE9B,QAAQ,EAAE,MAAM,CAAC;IAGjB,OAAO,EAAE,OAAO,CAAC;IAGjB,OAAO,EAAE,MAAM,CAAC;IAGhB,QAAQ,EAAE,MAAM,CAAC;IAGjB,aAAa,EAAE,MAAM,CAAC;IAGtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBACa,sBAAsB;IAEjC,QAAQ,EAAE,MAAM,CAAC;IAGjB,UAAU,EAAE,MAAM,CAAC;IAGnB,OAAO,EAAE,OAAO,CAAC;IAGjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBACa,sBAAuB,SAAQ,YAAY;IAEhD,aAAa,CAAgC,QAAQ,EAAE,MAAM,EAAS,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAkB9H;;OAEG;IAEG,oCAAoC,CACG,oBAAoB,EAAE,MAAM,EAChE,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,UAAU,GACxD,OAAO,CAAC,sBAAsB,CAAC;CAiFnC"}