@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3

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 (220) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +2189 -71
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +123 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +11 -23
  9. package/dist/context.js.map +1 -1
  10. package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
  11. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  12. package/dist/generated/graphql-schemas/__mj.js +24441 -14433
  13. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  14. package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
  15. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  16. package/dist/generic/FireAndForgetHeartbeat.js +16 -2
  17. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  18. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
  19. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
  20. package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
  21. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
  22. package/dist/generic/PushStatusResolver.d.ts +38 -0
  23. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  24. package/dist/generic/PushStatusResolver.js +84 -1
  25. package/dist/generic/PushStatusResolver.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +56 -4
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +191 -74
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +33 -1
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +204 -50
  37. package/dist/index.js.map +1 -1
  38. package/dist/postgresqlCompat.d.ts +8 -0
  39. package/dist/postgresqlCompat.d.ts.map +1 -0
  40. package/dist/postgresqlCompat.js +19 -0
  41. package/dist/postgresqlCompat.js.map +1 -0
  42. package/dist/postgresqlPoolSettings.d.ts +108 -0
  43. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  44. package/dist/postgresqlPoolSettings.js +134 -0
  45. package/dist/postgresqlPoolSettings.js.map +1 -0
  46. package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
  47. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  48. package/dist/resolvers/AdhocQueryResolver.js +77 -167
  49. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  50. package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
  51. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
  52. package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
  53. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
  54. package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
  55. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
  56. package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
  57. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
  58. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
  59. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
  60. package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
  61. package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
  62. package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
  63. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  64. package/dist/resolvers/EntityRecordNameResolver.js +9 -47
  65. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  66. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  67. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
  68. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  69. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  70. package/dist/resolvers/GetDataResolver.js +9 -0
  71. package/dist/resolvers/GetDataResolver.js.map +1 -1
  72. package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
  73. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
  74. package/dist/resolvers/HumanHandoffResolver.js +324 -0
  75. package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
  76. package/dist/resolvers/MeetingResolver.d.ts +69 -0
  77. package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
  78. package/dist/resolvers/MeetingResolver.js +381 -0
  79. package/dist/resolvers/MeetingResolver.js.map +1 -0
  80. package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
  81. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  82. package/dist/resolvers/MergeRecordsResolver.js +16 -2
  83. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  84. package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
  85. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  86. package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
  87. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
  89. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  90. package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
  91. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  92. package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
  93. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  94. package/dist/resolvers/RunAIAgentResolver.js +23 -153
  95. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  96. package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
  97. package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
  98. package/dist/resolvers/RunDecisionResolver.js +396 -0
  99. package/dist/resolvers/RunDecisionResolver.js.map +1 -0
  100. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  101. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  102. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  103. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  104. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  105. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  106. package/dist/resolvers/adhocTimeout.js +12 -0
  107. package/dist/resolvers/adhocTimeout.js.map +1 -0
  108. package/dist/resolvers/meetings/index.d.ts +5 -0
  109. package/dist/resolvers/meetings/index.d.ts.map +1 -0
  110. package/dist/resolvers/meetings/index.js +5 -0
  111. package/dist/resolvers/meetings/index.js.map +1 -0
  112. package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
  113. package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
  114. package/dist/resolvers/meetings/meetingHelpers.js +212 -0
  115. package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
  116. package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
  117. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
  118. package/dist/resolvers/meetings/meetingMutations.js +418 -0
  119. package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
  120. package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
  121. package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
  122. package/dist/resolvers/meetings/meetingQueries.js +113 -0
  123. package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
  124. package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
  125. package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
  126. package/dist/resolvers/meetings/meetingTypes.js +345 -0
  127. package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
  128. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  129. package/dist/rest/ViewOperationsHandler.js +8 -5
  130. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  131. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  132. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  133. package/dist/services/TaskGraphActionRunner.js +27 -1
  134. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  135. package/dist/services/WorkQueueHostService.d.ts +36 -0
  136. package/dist/services/WorkQueueHostService.d.ts.map +1 -0
  137. package/dist/services/WorkQueueHostService.js +97 -0
  138. package/dist/services/WorkQueueHostService.js.map +1 -0
  139. package/dist/services/workQueueConfig.d.ts +103 -0
  140. package/dist/services/workQueueConfig.d.ts.map +1 -0
  141. package/dist/services/workQueueConfig.js +48 -0
  142. package/dist/services/workQueueConfig.js.map +1 -0
  143. package/dist/types.d.ts +5 -0
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +95 -92
  147. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  148. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  149. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  150. package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
  151. package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
  152. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  153. package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
  154. package/src/__tests__/MeetingResolver.test.ts +374 -0
  155. package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
  156. package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
  157. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
  158. package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
  159. package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
  160. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
  161. package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
  162. package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
  163. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
  164. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
  165. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
  166. package/src/__tests__/RunDecisionResolver.test.ts +699 -0
  167. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
  168. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  169. package/src/__tests__/WorkQueueHostService.test.ts +133 -0
  170. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  171. package/src/__tests__/conversationRunEvents.test.ts +399 -0
  172. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
  173. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  174. package/src/__tests__/pushStatusFanOut.test.ts +206 -0
  175. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  176. package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
  177. package/src/config.ts +130 -3
  178. package/src/context.ts +11 -24
  179. package/src/generated/graphql-schemas/__mj.ts +24183 -17182
  180. package/src/generic/FireAndForgetHeartbeat.ts +16 -2
  181. package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
  182. package/src/generic/PushStatusResolver.ts +97 -1
  183. package/src/generic/ResolverBase.ts +220 -76
  184. package/src/generic/RunViewResolver.ts +33 -2
  185. package/src/index.ts +228 -51
  186. package/src/postgresqlCompat.ts +21 -0
  187. package/src/postgresqlPoolSettings.ts +213 -0
  188. package/src/resolvers/AdhocQueryResolver.ts +88 -188
  189. package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
  190. package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
  191. package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
  192. package/src/resolvers/EntityRecordNameResolver.ts +9 -51
  193. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
  194. package/src/resolvers/GetDataResolver.ts +10 -0
  195. package/src/resolvers/HumanHandoffResolver.ts +304 -0
  196. package/src/resolvers/MeetingResolver.ts +438 -0
  197. package/src/resolvers/MergeRecordsResolver.ts +14 -2
  198. package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
  199. package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
  200. package/src/resolvers/RunAIAgentResolver.ts +28 -174
  201. package/src/resolvers/RunDecisionResolver.ts +435 -0
  202. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  203. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
  204. package/src/resolvers/adhocTimeout.ts +12 -0
  205. package/src/resolvers/meetings/index.ts +4 -0
  206. package/src/resolvers/meetings/meetingHelpers.ts +307 -0
  207. package/src/resolvers/meetings/meetingMutations.ts +535 -0
  208. package/src/resolvers/meetings/meetingQueries.ts +172 -0
  209. package/src/resolvers/meetings/meetingTypes.ts +238 -0
  210. package/src/rest/ViewOperationsHandler.ts +8 -5
  211. package/src/services/TaskGraphActionRunner.ts +29 -1
  212. package/src/services/WorkQueueHostService.ts +129 -0
  213. package/src/services/workQueueConfig.ts +52 -0
  214. package/src/types.ts +5 -0
  215. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
  216. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  217. package/dist/resolvers/adhoc-query-helpers.js +0 -50
  218. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  219. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  220. package/src/resolvers/adhoc-query-helpers.ts +0 -67
@@ -1,5 +1,19 @@
1
1
  import { PubSubEngine } from 'type-graphql';
2
- /** Default cadence for fire-and-forget liveness pulses (5 minutes). */
2
+ /**
3
+ * Default cadence for fire-and-forget liveness pulses (60 seconds).
4
+ *
5
+ * MATCHED PAIR — do not change alone. The client's inactivity window
6
+ * (`DEFAULT_IDLE_TIMEOUT_MS` in GraphQLDataProvider's fireAndForgetHelper) is sized at roughly
7
+ * 3x this value, so a healthy run always refreshes the timer well before it expires. Lengthening
8
+ * this without lengthening that window makes every quiet stretch of a long agent run look like a
9
+ * stall and trips reconciliation; shortening the window without shortening this does the same.
10
+ *
11
+ * WAS 5 MINUTES (MJ #4222). Paired with the old 12-minute window, a client whose transport had
12
+ * silently died waited 12 minutes to notice — on runs that typically finish in 20 seconds. The
13
+ * pulse is a few dozen bytes and exists only while an operation is actually running, so the
14
+ * added traffic is negligible: a 40-minute run now sends ~40 pulses instead of ~8, and the
15
+ * 20-second run that motivated the change still sends none.
16
+ */
3
17
  export declare const DEFAULT_PULSE_INTERVAL_MS: number;
4
18
  /** The `type` discriminator carried by liveness pulse messages. */
5
19
  export declare const HEARTBEAT_MESSAGE_TYPE = "Heartbeat";
@@ -1 +1 @@
1
- {"version":3,"file":"FireAndForgetHeartbeat.d.ts","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAI5C,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,QAAgB,CAAC;AAEvD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IACxB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAChC,IAAI,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACjC,qEAAqE;IACrE,MAAM,EAAE,YAAY,CAAC;IACrB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CA4BrF;AAED,kDAAkD;AAClD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAErF"}
1
+ {"version":3,"file":"FireAndForgetHeartbeat.d.ts","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAI5C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAEnD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IACxB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAChC,IAAI,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACjC,qEAAqE;IACrE,MAAM,EAAE,YAAY,CAAC;IACrB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CA4BrF;AAED,kDAAkD;AAClD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAErF"}
@@ -1,7 +1,21 @@
1
1
  import { LogError } from '@memberjunction/core';
2
2
  import { PublishStatusUpdate } from './PushStatusResolver.js';
3
- /** Default cadence for fire-and-forget liveness pulses (5 minutes). */
4
- export const DEFAULT_PULSE_INTERVAL_MS = 5 * 60 * 1000;
3
+ /**
4
+ * Default cadence for fire-and-forget liveness pulses (60 seconds).
5
+ *
6
+ * MATCHED PAIR — do not change alone. The client's inactivity window
7
+ * (`DEFAULT_IDLE_TIMEOUT_MS` in GraphQLDataProvider's fireAndForgetHelper) is sized at roughly
8
+ * 3x this value, so a healthy run always refreshes the timer well before it expires. Lengthening
9
+ * this without lengthening that window makes every quiet stretch of a long agent run look like a
10
+ * stall and trips reconciliation; shortening the window without shortening this does the same.
11
+ *
12
+ * WAS 5 MINUTES (MJ #4222). Paired with the old 12-minute window, a client whose transport had
13
+ * silently died waited 12 minutes to notice — on runs that typically finish in 20 seconds. The
14
+ * pulse is a few dozen bytes and exists only while an operation is actually running, so the
15
+ * added traffic is negligible: a 40-minute run now sends ~40 pulses instead of ~8, and the
16
+ * 20-second run that motivated the change still sends none.
17
+ */
18
+ export const DEFAULT_PULSE_INTERVAL_MS = 60 * 1000;
5
19
  /** The `type` discriminator carried by liveness pulse messages. */
6
20
  export const HEARTBEAT_MESSAGE_TYPE = 'Heartbeat';
7
21
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"FireAndForgetHeartbeat.js","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,uEAAuE;AACvE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEvD,mEAAmE;AACnE,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAC;AA2ClD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC5D,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,yBAAyB,CAAC;IAEnE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC3B,IAAI,IAA6B,CAAC;QAClC,IAAI,CAAC;YACD,IAAI,GAAG,UAAU,EAAE,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,0DAA0D;YAC1D,QAAQ,CAAC,kBAAkB,QAAQ,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,mBAAmB,CAAC,MAAM,EAAE;YACxB,SAAS;YACT,WAAW;YACX,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;gBACpB,QAAQ;gBACR,IAAI,EAAE,sBAAsB;gBAC5B,MAAM,EAAE,IAAI;gBACZ,IAAI,EAAE,IAAI,IAAI,EAAE;aACnB,CAAC;SACL,CAAC,CAAC;IACP,CAAC,EAAE,UAAU,CAAC,CAAC;IAEf,OAAO;QACH,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC;KACnC,CAAC;AACN,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC5D,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"FireAndForgetHeartbeat.js","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD,mEAAmE;AACnE,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAC;AA2ClD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC5D,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,yBAAyB,CAAC;IAEnE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC3B,IAAI,IAA6B,CAAC;QAClC,IAAI,CAAC;YACD,IAAI,GAAG,UAAU,EAAE,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,0DAA0D;YAC1D,QAAQ,CAAC,kBAAkB,QAAQ,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,mBAAmB,CAAC,MAAM,EAAE;YACxB,SAAS;YACT,WAAW;YACX,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;gBACpB,QAAQ;gBACR,IAAI,EAAE,sBAAsB;gBAC5B,MAAM,EAAE,IAAI;gBACZ,IAAI,EAAE,IAAI,IAAI,EAAE;aACnB,CAAC;SACL,CAAC,CAAC;IACP,CAAC,EAAE,UAAU,CAAC,CAAC;IAEf,OAAO;QACH,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC;KACnC,CAAC;AACN,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC5D,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
2
+ /**
3
+ * Grace period after a run reaches a terminal state before its conversation detail is considered
4
+ * abandoned. The normal path closes the detail moments after the run finishes; this waits well past
5
+ * that so a reconcile can never race a completion that is already on its way.
6
+ */
7
+ export declare const ORPHAN_DETAIL_GRACE_MS: number;
8
+ /**
9
+ * Close conversation details left `In-Progress` by a run that is already over (MJ #4222).
10
+ *
11
+ * WHY THIS EXISTS. `AgentRunner` closes the detail as the final step of a run, so a process that
12
+ * dies mid-run never reaches it. The agent-run watchdog repairs the RUN — force-failing it once the
13
+ * heartbeat goes stale — but nothing repairs the detail, and the detail is the row the chat renders
14
+ * from. The result is durable state that disagrees with itself: a terminal run beside a message
15
+ * that still says it is being generated, spinning forever for anyone who opens it.
16
+ *
17
+ * WHY NOT IN THE SWEEP PROC. `spSweepStaleAIAgentRuns` is a single set-based UPDATE, and extending
18
+ * it to the detail would be the obvious fix. It would also be the wrong one: `MJ: Conversation
19
+ * Details` declares `TrustServerCacheCompletely`, so the server's RunView cache relies on
20
+ * `BaseEntity` events for invalidation and a stored procedure fires none. The row would change
21
+ * while every instance kept serving `In-Progress` from cache — a worse failure than the one being
22
+ * fixed, because reconciliation itself would read the stale value back. Going through
23
+ * `BaseEntity.Save()` costs a per-row write and buys correct invalidation everywhere.
24
+ *
25
+ * Idempotent and safe to run concurrently on several instances: the write is a no-op once the
26
+ * status is terminal, and each pass re-reads state before deciding.
27
+ *
28
+ * @returns How many details were closed.
29
+ */
30
+ export declare function ReconcileOrphanedConversationDetails(provider: IMetadataProvider, contextUser: UserInfo, graceMs?: number): Promise<number>;
31
+ //# sourceMappingURL=OrphanedConversationDetailReconciler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OrphanedConversationDetailReconciler.d.ts","sourceRoot":"","sources":["../../src/generic/OrphanedConversationDetailReconciler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGxE;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAiBpD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oCAAoC,CACtD,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,QAAQ,EACrB,OAAO,GAAE,MAA+B,GACzC,OAAO,CAAC,MAAM,CAAC,CAkGjB"}
@@ -0,0 +1,215 @@
1
+ import { LogError, LogStatus, RunView } from '@memberjunction/core';
2
+ import { UUIDsEqual } from '@memberjunction/global';
3
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
4
+ /**
5
+ * Grace period after a run reaches a terminal state before its conversation detail is considered
6
+ * abandoned. The normal path closes the detail moments after the run finishes; this waits well past
7
+ * that so a reconcile can never race a completion that is already on its way.
8
+ */
9
+ export const ORPHAN_DETAIL_GRACE_MS = 2 * 60 * 1000;
10
+ /** Ceiling on details closed per pass, so a large backlog cannot stall startup. */
11
+ const MAX_DETAILS_PER_PASS = 200;
12
+ /**
13
+ * Agent-run statuses that mean the run succeeded. `AwaitingFeedback` is the normal end of a chat
14
+ * turn: the agent replied and the next move is the user's.
15
+ */
16
+ const SUCCESSFUL_RUN_STATUSES = ['Completed', 'AwaitingFeedback'];
17
+ /**
18
+ * Agent-run statuses that mean execution is over, whatever the outcome. `Paused` is absent: the
19
+ * run is parked on a workflow that is still executing.
20
+ */
21
+ const TERMINAL_RUN_STATUSES = [...SUCCESSFUL_RUN_STATUSES, 'Failed', 'Cancelled'];
22
+ /**
23
+ * Close conversation details left `In-Progress` by a run that is already over (MJ #4222).
24
+ *
25
+ * WHY THIS EXISTS. `AgentRunner` closes the detail as the final step of a run, so a process that
26
+ * dies mid-run never reaches it. The agent-run watchdog repairs the RUN — force-failing it once the
27
+ * heartbeat goes stale — but nothing repairs the detail, and the detail is the row the chat renders
28
+ * from. The result is durable state that disagrees with itself: a terminal run beside a message
29
+ * that still says it is being generated, spinning forever for anyone who opens it.
30
+ *
31
+ * WHY NOT IN THE SWEEP PROC. `spSweepStaleAIAgentRuns` is a single set-based UPDATE, and extending
32
+ * it to the detail would be the obvious fix. It would also be the wrong one: `MJ: Conversation
33
+ * Details` declares `TrustServerCacheCompletely`, so the server's RunView cache relies on
34
+ * `BaseEntity` events for invalidation and a stored procedure fires none. The row would change
35
+ * while every instance kept serving `In-Progress` from cache — a worse failure than the one being
36
+ * fixed, because reconciliation itself would read the stale value back. Going through
37
+ * `BaseEntity.Save()` costs a per-row write and buys correct invalidation everywhere.
38
+ *
39
+ * Idempotent and safe to run concurrently on several instances: the write is a no-op once the
40
+ * status is terminal, and each pass re-reads state before deciding.
41
+ *
42
+ * @returns How many details were closed.
43
+ */
44
+ export async function ReconcileOrphanedConversationDetails(provider, contextUser, graceMs = ORPHAN_DETAIL_GRACE_MS) {
45
+ try {
46
+ const rv = RunView.FromMetadataProvider(provider);
47
+ const detailResult = await rv.RunView({
48
+ EntityName: 'MJ: Conversation Details',
49
+ ExtraFilter: `Status = 'In-Progress' AND Role = 'AI'${hasTerminalRunClause(provider)}`,
50
+ OrderBy: '__mj_CreatedAt ASC',
51
+ MaxRows: MAX_DETAILS_PER_PASS,
52
+ ResultType: 'entity_object',
53
+ }, contextUser);
54
+ if (!detailResult.Success || !detailResult.Results?.length) {
55
+ return 0;
56
+ }
57
+ const runsByDetail = await loadTerminalRuns(rv, detailResult.Results.map(d => d.ID), contextUser);
58
+ if (runsByDetail.size === 0) {
59
+ return 0;
60
+ }
61
+ const ownersByConversation = await loadConversationOwners(rv, detailResult.Results.map(d => d.ConversationID).filter((id) => !!id), contextUser);
62
+ const cutoff = Date.now() - graceMs;
63
+ let closed = 0;
64
+ for (const detail of detailResult.Results) {
65
+ const run = runsByDetail.get(detail.ID);
66
+ if (!run) {
67
+ // No run yet, or its run is still executing. Both are legitimately in progress.
68
+ continue;
69
+ }
70
+ const completedAt = run.CompletedAt ? new Date(run.CompletedAt).getTime() : NaN;
71
+ if (!Number.isFinite(completedAt) || completedAt > cutoff) {
72
+ // Inside the grace window — the owning process may still be closing this itself.
73
+ continue;
74
+ }
75
+ // Write as the conversation's OWNER, not as the system user.
76
+ //
77
+ // `MJConversationDetailEntityExtended.Save()` gates writes on conversation ownership: a
78
+ // non-owner without a resource grant is refused, returning false with no result
79
+ // registered anywhere — indistinguishable from a crash, and the reason this pass
80
+ // silently wrote nothing at all until it was traced. The system user owns no
81
+ // conversations, so it is exactly the caller that gate exists to stop. Saving as the
82
+ // owner satisfies the gate on its own terms rather than routing around it, and keeps
83
+ // the write attributable to the person whose message it is.
84
+ const owner = detail.ConversationID ? ownersByConversation.get(detail.ConversationID) : undefined;
85
+ if (!owner) {
86
+ LogError(`[OrphanDetailReconciler] No owner resolved for conversation ${detail.ConversationID}; leaving detail ${detail.ID}`);
87
+ continue;
88
+ }
89
+ const writable = await provider.GetEntityObject('MJ: Conversation Details', owner);
90
+ if (!(await writable.Load(detail.ID))) {
91
+ LogError(`[OrphanDetailReconciler] Could not load detail ${detail.ID} for write`);
92
+ continue;
93
+ }
94
+ if (writable.Status !== 'In-Progress') {
95
+ // Closed by something else since the list query. Its outcome stands.
96
+ continue;
97
+ }
98
+ // The failure marker belongs only to the error branch. A completed run has nothing to
99
+ // report, so an empty message stays empty rather than contradicting its own status.
100
+ if (SUCCESSFUL_RUN_STATUSES.includes(run.Status)) {
101
+ writable.Status = 'Complete';
102
+ }
103
+ else {
104
+ writable.Status = 'Error';
105
+ if (!writable.Message) {
106
+ writable.Message = run.ErrorMessage ?? '❌ Failed';
107
+ }
108
+ }
109
+ if (await writable.Save()) {
110
+ closed++;
111
+ }
112
+ else {
113
+ const result = writable.LatestResult;
114
+ LogError(`[OrphanDetailReconciler] Failed to close detail ${detail.ID}: ` +
115
+ `${result?.CompleteMessage || result?.Message || 'no message'}`);
116
+ }
117
+ }
118
+ if (closed > 0) {
119
+ LogStatus(`[OrphanDetailReconciler] Closed ${closed} conversation detail(s) left behind by finished runs`);
120
+ }
121
+ return closed;
122
+ }
123
+ catch (err) {
124
+ // Maintenance work must never take the server down with it.
125
+ LogError(`[OrphanDetailReconciler] Pass failed: ${err instanceof Error ? err.message : String(err)}`);
126
+ return 0;
127
+ }
128
+ }
129
+ function isQuotingProvider(provider) {
130
+ const p = provider;
131
+ return typeof p.QuoteIdentifier === 'function' && typeof p.QuoteSchemaAndView === 'function';
132
+ }
133
+ /**
134
+ * Filter clause that keeps only details with at least one finished run.
135
+ *
136
+ * Details with no run, or whose run is still executing, are never closed here. Without this clause
137
+ * they fill the oldest-first window, and once 200 of them accumulate no closable orphan is reached.
138
+ * The newest-run check in {@link loadTerminalRuns} still decides each remaining detail.
139
+ *
140
+ * @returns An ` AND ...` clause, or an empty string when the provider cannot quote names.
141
+ */
142
+ function hasTerminalRunClause(provider) {
143
+ const runs = provider.EntityByName('MJ: AI Agent Runs');
144
+ if (!runs || !isQuotingProvider(provider)) {
145
+ return '';
146
+ }
147
+ const statuses = TERMINAL_RUN_STATUSES.map(s => `'${s}'`).join(',');
148
+ return (` AND ID IN (SELECT ${provider.QuoteIdentifier('ConversationDetailID')}` +
149
+ ` FROM ${provider.QuoteSchemaAndView(runs.SchemaName, runs.BaseView)}` +
150
+ ` WHERE ${provider.QuoteIdentifier('Status')} IN (${statuses}))`);
151
+ }
152
+ /**
153
+ * Newest terminal run per conversation detail. Details whose newest run is still executing are
154
+ * deliberately absent, so a retried message is judged on its latest attempt rather than an older
155
+ * finished one.
156
+ */
157
+ async function loadTerminalRuns(rv, detailIds, contextUser) {
158
+ const quoted = detailIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',');
159
+ const result = await rv.RunView({
160
+ EntityName: 'MJ: AI Agent Runs',
161
+ ExtraFilter: `ConversationDetailID IN (${quoted})`,
162
+ OrderBy: '__mj_CreatedAt DESC',
163
+ ResultType: 'entity_object',
164
+ }, contextUser);
165
+ if (!result.Success) {
166
+ return new Map();
167
+ }
168
+ const newest = new Map();
169
+ for (const run of result.Results ?? []) {
170
+ const detailId = run.ConversationDetailID;
171
+ if (!detailId || newest.has(detailId)) {
172
+ continue; // DESC order puts the newest run first
173
+ }
174
+ newest.set(detailId, run);
175
+ }
176
+ // Drop any detail whose newest attempt is still running.
177
+ for (const [detailId, run] of newest) {
178
+ if (!TERMINAL_RUN_STATUSES.includes(run.Status)) {
179
+ newest.delete(detailId);
180
+ }
181
+ }
182
+ return newest;
183
+ }
184
+ /**
185
+ * Owner `UserInfo` per conversation, resolved through {@link UserCache}. The reconciler writes as
186
+ * the owner so the conversation-detail permission gate is satisfied on its own terms.
187
+ */
188
+ async function loadConversationOwners(rv, conversationIds, contextUser) {
189
+ const owners = new Map();
190
+ if (conversationIds.length === 0) {
191
+ return owners;
192
+ }
193
+ const quoted = [...new Set(conversationIds)].map(id => `'${id.replace(/'/g, "''")}'`).join(',');
194
+ const result = await rv.RunView({
195
+ EntityName: 'MJ: Conversations',
196
+ ExtraFilter: `ID IN (${quoted})`,
197
+ Fields: ['ID', 'UserID'],
198
+ ResultType: 'simple',
199
+ }, contextUser);
200
+ if (!result.Success) {
201
+ return owners;
202
+ }
203
+ const users = UserCache.Instance.Users;
204
+ for (const row of result.Results ?? []) {
205
+ if (!row.UserID) {
206
+ continue;
207
+ }
208
+ const user = users.find(u => UUIDsEqual(u.ID, row.UserID));
209
+ if (user) {
210
+ owners.set(row.ID, user);
211
+ }
212
+ }
213
+ return owners;
214
+ }
215
+ //# sourceMappingURL=OrphanedConversationDetailReconciler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OrphanedConversationDetailReconciler.js","sourceRoot":"","sources":["../../src/generic/OrphanedConversationDetailReconciler.ts"],"names":[],"mappings":"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"}
@@ -22,7 +22,45 @@ export interface PushStatusNotificationPayload {
22
22
  sessionId: string;
23
23
  /** Authenticated user the update belongs to. Server-side filter key; never sent to the client. */
24
24
  ownerUserId: string;
25
+ /**
26
+ * `MJGlobal.ProcessUUID` of the instance that first published this update. Used only for
27
+ * cross-instance echo suppression: an instance that receives its own message back from the
28
+ * message bus must drop it, or every push is delivered twice.
29
+ */
30
+ SourceServerId?: string;
25
31
  }
32
+ /** Receives every locally-published status update, for fan-out to other server instances. */
33
+ export type PushStatusPublishHook = (payload: PushStatusNotificationPayload) => void;
34
+ /**
35
+ * Register (or clear, with no arg) the cross-instance fan-out hook.
36
+ *
37
+ * Inversion of control, deliberately: this module must not know that Redis exists. The server
38
+ * registers the hook at startup only when a message bus is configured, so a single-instance
39
+ * deployment behaves exactly as before.
40
+ */
41
+ export declare function SetPushStatusPublishHook(hook?: PushStatusPublishHook): void;
42
+ /**
43
+ * Validate an update received from the cross-instance message bus.
44
+ *
45
+ * The bus carries messages from other processes, so every field is type-checked before the update
46
+ * is republished onto the local topic, where {@link StatusUpdatesFilter} compares `ownerUserId`
47
+ * with the connection's user.
48
+ *
49
+ * @param raw The JSON text taken off the bus.
50
+ * @param localServerId This process's `MJGlobal.ProcessUUID`, used to drop our own echo.
51
+ * @returns The update to republish, or `null` to drop it.
52
+ */
53
+ export declare function ParseReplicatedStatusUpdate(raw: string, localServerId: string): PushStatusNotificationPayload | null;
54
+ /**
55
+ * Whether an update is worth sending to other server instances.
56
+ *
57
+ * Streaming content is excluded. It arrives at hundreds of messages per second and each one carries
58
+ * a full serialized agent run, so replicating it would cost far more bandwidth than the delivery it
59
+ * buys — and a token delta is worthless to a client that has already missed the ones before it.
60
+ * Progress and completion are low-rate and each is individually meaningful, which is exactly the
61
+ * kind of message that must survive landing on the wrong replica.
62
+ */
63
+ export declare function ShouldReplicateStatusUpdate(message?: string): boolean;
26
64
  interface PushStatusNotificationArgs {
27
65
  sessionId: string;
28
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PushStatusResolver.d.ts","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,YAAY,EAAoD,MAAM,cAAc,CAAC;AAE1H,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D,qBACa,sBAAsB;IAEjC,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,IAAI,EAAG,IAAI,CAAC;IAGZ,SAAS,EAAG,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mGAAmG;AACnG,MAAM,WAAW,kBAAkB;IACjC,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;IACpB,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAO1F;AAED,mDAAmD;AACnD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAE1F;AAED,mFAAmF;AACnF,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,6BAA6B,CAAC;IACvC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;CACjD,GAAG,OAAO,CAUV;AAED,mDAAmD;AACnD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,6BAA6B,CAAC;IACvC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;CACjD,GAAG,OAAO,CAEV;AAED,qBACa,kBAAkB;IAM7B,aAAa,CAAG,mIAAmI;IACzI,EAAE,OAAO,EAAE,EAAE,6BAA6B,EAClB,SAAS,EAAE,MAAM,GAChD,sBAAsB;CAI1B"}
1
+ {"version":3,"file":"PushStatusResolver.d.ts","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,YAAY,EAAoD,MAAM,cAAc,CAAC;AAE1H,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D,qBACa,sBAAsB;IAEjC,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,IAAI,EAAG,IAAI,CAAC;IAGZ,SAAS,EAAG,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,6FAA6F;AAC7F,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,IAAI,CAAC;AAIrF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAE3E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,6BAA6B,GAAG,IAAI,CAyBpH;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAYrE;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mGAAmG;AACnG,MAAM,WAAW,kBAAkB;IACjC,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;IACpB,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAoB1F;AAED,mDAAmD;AACnD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAE1F;AAED,mFAAmF;AACnF,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,6BAA6B,CAAC;IACvC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;CACjD,GAAG,OAAO,CAUV;AAED,mDAAmD;AACnD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,6BAA6B,CAAC;IACvC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;CACjD,GAAG,OAAO,CAEV;AAED,qBACa,kBAAkB;IAM7B,aAAa,CAAG,mIAAmI;IACzI,EAAE,OAAO,EAAE,EAAE,6BAA6B,EAClB,SAAS,EAAE,MAAM,GAChD,sBAAsB;CAI1B"}
@@ -11,7 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
13
  import { Arg, Field, ID, ObjectType, Resolver, Root, Subscription } from 'type-graphql';
14
- import { UUIDsEqual } from '@memberjunction/global';
14
+ import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
15
15
  export const PUSH_STATUS_UPDATES_TOPIC = 'PUSH_STATUS_UPDATES';
16
16
  let PushStatusNotification = class PushStatusNotification {
17
17
  };
@@ -31,6 +31,76 @@ PushStatusNotification = __decorate([
31
31
  ObjectType()
32
32
  ], PushStatusNotification);
33
33
  export { PushStatusNotification };
34
+ let _publishHook;
35
+ /**
36
+ * Register (or clear, with no arg) the cross-instance fan-out hook.
37
+ *
38
+ * Inversion of control, deliberately: this module must not know that Redis exists. The server
39
+ * registers the hook at startup only when a message bus is configured, so a single-instance
40
+ * deployment behaves exactly as before.
41
+ */
42
+ export function SetPushStatusPublishHook(hook) {
43
+ _publishHook = hook;
44
+ }
45
+ /**
46
+ * Validate an update received from the cross-instance message bus.
47
+ *
48
+ * The bus carries messages from other processes, so every field is type-checked before the update
49
+ * is republished onto the local topic, where {@link StatusUpdatesFilter} compares `ownerUserId`
50
+ * with the connection's user.
51
+ *
52
+ * @param raw The JSON text taken off the bus.
53
+ * @param localServerId This process's `MJGlobal.ProcessUUID`, used to drop our own echo.
54
+ * @returns The update to republish, or `null` to drop it.
55
+ */
56
+ export function ParseReplicatedStatusUpdate(raw, localServerId) {
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(raw);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ if (typeof parsed !== 'object' || parsed === null) {
65
+ return null;
66
+ }
67
+ const { sessionId, ownerUserId, message, SourceServerId } = parsed;
68
+ const source = typeof SourceServerId === 'string' ? SourceServerId : undefined;
69
+ const body = typeof message === 'string' ? message : undefined;
70
+ if ((SourceServerId != null && source === undefined) || (message != null && body === undefined)) {
71
+ return null; // a present field of the wrong type
72
+ }
73
+ if (source === localServerId) {
74
+ return null; // our own message, echoed back
75
+ }
76
+ if (typeof sessionId !== 'string' || !sessionId || typeof ownerUserId !== 'string' || !ownerUserId) {
77
+ return null; // fail closed: an update with no valid identity can never be routed safely
78
+ }
79
+ return { sessionId, ownerUserId, message: body, SourceServerId: source };
80
+ }
81
+ /**
82
+ * Whether an update is worth sending to other server instances.
83
+ *
84
+ * Streaming content is excluded. It arrives at hundreds of messages per second and each one carries
85
+ * a full serialized agent run, so replicating it would cost far more bandwidth than the delivery it
86
+ * buys — and a token delta is worthless to a client that has already missed the ones before it.
87
+ * Progress and completion are low-rate and each is individually meaningful, which is exactly the
88
+ * kind of message that must survive landing on the wrong replica.
89
+ */
90
+ export function ShouldReplicateStatusUpdate(message) {
91
+ if (!message) {
92
+ return false;
93
+ }
94
+ try {
95
+ const parsed = JSON.parse(message);
96
+ const type = parsed?.type;
97
+ return type !== 'StreamingContent';
98
+ }
99
+ catch {
100
+ // A plain-string message (not the resolvers' JSON envelope) is rare and low-rate. Replicate it.
101
+ return true;
102
+ }
103
+ }
34
104
  /**
35
105
  * The single, centralized vehicle for publishing on {@link PUSH_STATUS_UPDATES_TOPIC}. Every
36
106
  * publisher — resolver or service — routes through here (resolvers via the
@@ -43,8 +113,21 @@ export function PublishStatusUpdate(pubSub, params) {
43
113
  sessionId: params.sessionId,
44
114
  ownerUserId: params.ownerUserId,
45
115
  message: params.message,
116
+ SourceServerId: MJGlobal.Instance.ProcessUUID,
46
117
  };
47
118
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
119
+ // Fan out to other instances. Only reached by updates originating HERE: a message arriving from
120
+ // the bus is republished straight onto the local topic and never comes back through this
121
+ // function, so there is no loop to break beyond the SourceServerId check on the receiving side.
122
+ if (_publishHook && ShouldReplicateStatusUpdate(params.message)) {
123
+ try {
124
+ _publishHook(payload);
125
+ }
126
+ catch {
127
+ // Fan-out is an optimization over the durable tail query. A message bus that is down must
128
+ // never break local delivery, which is the path that works for the common case.
129
+ }
130
+ }
48
131
  }
49
132
  /** @deprecated Use {@link PublishStatusUpdate}. */
50
133
  export function publishStatusUpdate(pubSub, params) {
@@ -1 +1 @@
1
- {"version":3,"file":"PushStatusResolver.js","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAgB,QAAQ,EAAsB,IAAI,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1H,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGpD,MAAM,CAAC,MAAM,yBAAyB,GAAG,qBAAqB,CAAC;AAGxD,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;CASlC,CAAA;AAPC;IADC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;uDACvB;AAGjB;IADC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC;8BAChB,IAAI;oDAAC;AAGZ;IADC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;;yDACF;AARR,sBAAsB;IADlC,UAAU,EAAE;GACA,sBAAsB,CASlC;;AAkCD;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,MAA0B;IAClF,MAAM,OAAO,GAAkC;QAC7C,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC;IACF,MAAM,CAAC,OAAO,CAAC,yBAAyB,EAAE,OAAO,CAAC,CAAC;AACrD,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,MAA0B;IAClF,OAAO,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC;AAOD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAInC;IACC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACxC,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,gBAAgB,GAAuB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE,CAAC;IAClF,IAAI,CAAC,gBAAgB,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC9C,OAAO,KAAK,CAAC,CAAC,6DAA6D;IAC7E,CAAC;IACD,OAAO,UAAU,CAAC,OAAO,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;AAC3D,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,mBAAmB,CAAC,IAInC;IACC,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAGM,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAM7B,aAAa,CACH,EAAE,OAAO,EAAiC,EAClB,SAAiB;QAEjD,yFAAyF;QACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC;IAClD,CAAC;CACF,CAAA;AAPC;IALC,YAAY,CAAC,GAAG,EAAE,CAAC,sBAAsB,EAAE;QAC1C,MAAM,EAAE,yBAAyB;QACjC,MAAM,EAAE,CAAC,IAA+G,EAAE,EAAE,CAC1H,mBAAmB,CAAC,IAAI,CAAC;KAC5B,CAAC;IAEC,WAAA,IAAI,EAAE,CAAA;IACN,WAAA,GAAG,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAA;;;oCAC9B,sBAAsB;uDAGxB;AAZU,kBAAkB;IAD9B,QAAQ,EAAE;GACE,kBAAkB,CAa9B"}
1
+ {"version":3,"file":"PushStatusResolver.js","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { AggregateExpression, BaseEntity, CompositeKey, DatabaseProviderBase, EntityInfo, EntityPermissionType, IMetadataProvider, RecordChangeFieldSecurityProjector, RunViewResult, UserInfo } from '@memberjunction/core';
1
+ import { AggregateExpression, BaseEntity, CompositeKey, DatabaseProviderBase, EntityFieldInfo, EntityInfo, EntityPermissionType, IMetadataProvider, RecordChangeFieldSecurityProjector, RunViewResult, UserInfo } from '@memberjunction/core';
2
2
  import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
3
3
  import { PubSubEngine } from 'type-graphql';
4
4
  import { RunViewGenericParams, UserPayload } from '../types.js';
@@ -169,7 +169,24 @@ export declare class ResolverBase {
169
169
  * into the view. Subqueries against another entity's BaseView therefore do not inherit that
170
170
  * entity's RLS; they are still restricted to the view (not the table).
171
171
  */
172
- protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider): void;
172
+ protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider, user?: UserInfo): void;
173
+ /**
174
+ * SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
175
+ * to the SELECT-only validation the caller performs, every table reference must resolve to an
176
+ * entity BaseView the acting user can read UNSCOPED. CTE names defined by the statement itself
177
+ * are excluded from the check. Fails closed: no user, or an unresolvable reference, refuses.
178
+ *
179
+ * Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
180
+ * row-level security and denied-field projection, and raw SQL applies neither. Rather than try
181
+ * to rewrite arbitrary SQL (joins, CTEs) to compose that policy, this refuses any entity that
182
+ * carries a row filter or a denied field for the caller — those reads belong on RunView. For
183
+ * the remaining entities CanRead genuinely means "every row and field of the view".
184
+ */
185
+ protected assertFullQueryUsesReadableEntityViews(sqlText: string, provider: IMetadataProvider | undefined, user: UserInfo | undefined, label?: string): void;
186
+ /** Refuses an entity whose read is narrowed for this user by row-level security or field-level denials. */
187
+ private assertUnscopedRead;
188
+ /** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
189
+ private collectCTENames;
173
190
  /**
174
191
  * Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
175
192
  * a view request can carry that is actually a SQL fragment. GraphQL entry points
@@ -197,19 +214,42 @@ export declare class ResolverBase {
197
214
  orderBy?: string | null;
198
215
  userSearchString?: string | null;
199
216
  overrideExcludeFilter?: string | null;
200
- }, provider?: IMetadataProvider): void;
217
+ }, provider?: IMetadataProvider, user?: UserInfo): void;
201
218
  /** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
202
219
  private astContainsWriteNode;
203
220
  private static readonly WRITE_NODE_TYPES;
204
221
  private dialectForProvider;
222
+ /** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
223
+ private isUnqualifiedSchema;
205
224
  private stripSqlIdent;
225
+ /**
226
+ * `bare` maps an unqualified view name to its entity, or to `null` when the name exists in
227
+ * more than one schema — the database, not this screen, would decide which one runs.
228
+ */
206
229
  private entityBaseViewAllowList;
230
+ /**
231
+ * SECURITY — resolves a table reference against the entity BaseView allow-list and, when an
232
+ * acting user is supplied, additionally requires that user to hold CanRead on the referenced
233
+ * entity. Base views do not embed RLS and entity permissions are otherwise checked only on
234
+ * the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
235
+ * entities the caller has no read grant on.
236
+ *
237
+ * A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
238
+ * through another schema's `vwFoo`. An unqualified reference resolves only when the view name
239
+ * is unique across schemas; an ambiguous one must be qualified, because the database's
240
+ * default-schema resolution, not this screen, would pick which view actually runs.
241
+ *
242
+ * `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
243
+ * indistinguishable from "no schema" here and both take the unqualified path (an exact
244
+ * `dbo.<view>` entity wins first).
245
+ */
246
+ private assertTableRefReadable;
207
247
  /**
208
248
  * Optimized RunViewGenericInternal implementation with:
209
249
  * - Field filtering at source (Fix #7)
210
250
  * - Improved error handling (Fix #9)
211
251
  */
212
- protected RunViewGenericInternal(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, extraFilter: string, orderBy: string, userSearchString: string, excludeUserViewRunID: string | undefined, overrideExcludeFilter: string | undefined, saveViewResults: boolean | undefined, fields: string[] | undefined, ignoreMaxRows: boolean | undefined, excludeDataFromAllPriorViewRuns: boolean | undefined, forceAuditLog: boolean | undefined, auditLogDescription: string | undefined, resultType: string | undefined, userPayload: UserPayload | null, maxRows: number | undefined, startRow: number | undefined, aggregates?: AggregateExpression[], afterKey?: CompositeKey, bypassCache?: boolean, dataSource?: 'Live' | 'Materialized'): Promise<RunViewResult<any>>;
252
+ protected RunViewGenericInternal(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, extraFilter: string, orderBy: string, userSearchString: string, excludeUserViewRunID: string | undefined, overrideExcludeFilter: string | undefined, saveViewResults: boolean | undefined, fields: string[] | undefined, ignoreMaxRows: boolean | undefined, excludeDataFromAllPriorViewRuns: boolean | undefined, forceAuditLog: boolean | undefined, auditLogDescription: string | undefined, resultType: string | undefined, userPayload: UserPayload | null, maxRows: number | undefined, startRow: number | undefined, aggregates?: AggregateExpression[], afterKey?: CompositeKey, bypassCache?: boolean, dataSource?: 'Live' | 'Materialized', includeBinaryFields?: boolean): Promise<RunViewResult<any>>;
213
253
  /**
214
254
  * Optimized implementation that:
215
255
  * 1. Fetches user info only once (fixes N+1 query)
@@ -301,6 +341,11 @@ export declare class ResolverBase {
301
341
  *
302
342
  * Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have
303
343
  * the feature switched on, which is almost none of them.
344
+ *
345
+ * `MJ: Record Changes` always loads from the database: its server class allows an update only when
346
+ * Comments is the one dirty field, and "dirty" compared against client-supplied OldValues lets a
347
+ * caller pin forged audit columns as both old and new values. It doesn't track its own changes, so
348
+ * nothing else forces the load.
304
349
  */
305
350
  protected MustLoadTruthFromDatabase(entityInfo: EntityInfo, input: {
306
351
  OldValues___?: Array<{
@@ -362,6 +407,13 @@ export declare class ResolverBase {
362
407
  Value: unknown;
363
408
  }>;
364
409
  } & Record<string, unknown>, clientNewValues: Record<string, unknown>, provider?: IMetadataProvider): boolean;
410
+ /**
411
+ * Converts one client-sent old value (always a string on the wire; dates as epoch milliseconds,
412
+ * the GraphQL Timestamp form) to the field's TypeScript type, so it compares equal to the stored
413
+ * value. Both the OldValues comparison and the load-from-OldValues path use it: without it a date
414
+ * old value becomes an Invalid Date and an unchanged date field reads as edited.
415
+ */
416
+ protected ClientOldValueToFieldValue(field: EntityFieldInfo | undefined, raw: unknown): string | number | boolean | Date | null;
365
417
  /**
366
418
  * This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client
367
419
  * is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values