@memberjunction/server 5.42.0 → 5.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  2. package/dist/agentSessions/SessionJanitor.js +5 -1
  3. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  4. package/dist/agentSessions/SessionManager.d.ts +15 -2
  5. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  6. package/dist/agentSessions/SessionManager.js +53 -11
  7. package/dist/agentSessions/SessionManager.js.map +1 -1
  8. package/dist/agentSessions/index.d.ts.map +1 -1
  9. package/dist/agentSessions/index.js +3 -1
  10. package/dist/agentSessions/index.js.map +1 -1
  11. package/dist/generated/generated.d.ts +1180 -51
  12. package/dist/generated/generated.d.ts.map +1 -1
  13. package/dist/generated/generated.js +35564 -29262
  14. package/dist/generated/generated.js.map +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +14 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +20 -1
  20. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
  21. package/dist/resolvers/ExecuteRemoteOperationResolver.js +61 -5
  22. package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
  23. package/dist/resolvers/FileResolver.d.ts +74 -0
  24. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  25. package/dist/resolvers/FileResolver.js +211 -2
  26. package/dist/resolvers/FileResolver.js.map +1 -1
  27. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  28. package/dist/resolvers/IntegrationDiscoveryResolver.js +98 -29
  29. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  30. package/dist/resolvers/QueryResolver.d.ts +6 -4
  31. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  32. package/dist/resolvers/QueryResolver.js +29 -14
  33. package/dist/resolvers/QueryResolver.js.map +1 -1
  34. package/dist/resolvers/RealtimeBridgeResolver.d.ts +17 -1
  35. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  36. package/dist/resolvers/RealtimeBridgeResolver.js +78 -7
  37. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  38. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +81 -2
  39. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  40. package/dist/resolvers/RealtimeClientSessionResolver.js +293 -17
  41. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  42. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  43. package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
  44. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  45. package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
  46. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  47. package/dist/resolvers/RunAIAgentResolver.js +37 -15
  48. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  49. package/dist/resolvers/RunTemplateResolver.d.ts +8 -0
  50. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  51. package/dist/resolvers/RunTemplateResolver.js +8 -0
  52. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  53. package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
  54. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
  55. package/dist/resolvers/meetingRecordingRegistration.js +311 -0
  56. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
  57. package/dist/resolvers/peaksSidecar.d.ts +30 -0
  58. package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
  59. package/dist/resolvers/peaksSidecar.js +51 -0
  60. package/dist/resolvers/peaksSidecar.js.map +1 -0
  61. package/dist/rest/MediaAccessKeys.d.ts +68 -0
  62. package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
  63. package/dist/rest/MediaAccessKeys.js +96 -0
  64. package/dist/rest/MediaAccessKeys.js.map +1 -0
  65. package/dist/rest/MediaStreamHandler.d.ts +26 -0
  66. package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
  67. package/dist/rest/MediaStreamHandler.js +192 -0
  68. package/dist/rest/MediaStreamHandler.js.map +1 -0
  69. package/dist/rest/mediaRange.d.ts +41 -0
  70. package/dist/rest/mediaRange.d.ts.map +1 -0
  71. package/dist/rest/mediaRange.js +60 -0
  72. package/dist/rest/mediaRange.js.map +1 -0
  73. package/package.json +83 -82
  74. package/src/__tests__/FileResolverPeaks.test.ts +64 -0
  75. package/src/__tests__/MediaAccessKeys.test.ts +68 -0
  76. package/src/__tests__/MediaStreamHandler.test.ts +91 -0
  77. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
  78. package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
  79. package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
  80. package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
  81. package/src/__tests__/SessionManager.test.ts +2 -0
  82. package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
  83. package/src/agentSessions/SessionJanitor.ts +15 -1
  84. package/src/agentSessions/SessionManager.ts +62 -10
  85. package/src/agentSessions/index.ts +3 -1
  86. package/src/generated/generated.ts +17659 -13322
  87. package/src/index.ts +15 -1
  88. package/src/resolvers/ExecuteRemoteOperationResolver.ts +60 -4
  89. package/src/resolvers/FileResolver.ts +199 -1
  90. package/src/resolvers/IntegrationDiscoveryResolver.ts +108 -30
  91. package/src/resolvers/QueryResolver.ts +33 -19
  92. package/src/resolvers/RealtimeBridgeResolver.ts +82 -7
  93. package/src/resolvers/RealtimeClientSessionResolver.ts +326 -9
  94. package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
  95. package/src/resolvers/RunAIAgentResolver.ts +44 -11
  96. package/src/resolvers/RunTemplateResolver.ts +8 -0
  97. package/src/resolvers/meetingRecordingRegistration.ts +432 -0
  98. package/src/resolvers/peaksSidecar.ts +52 -0
  99. package/src/rest/MediaAccessKeys.ts +121 -0
  100. package/src/rest/MediaStreamHandler.ts +220 -0
  101. package/src/rest/mediaRange.ts +76 -0
@@ -1,5 +1,5 @@
1
1
  import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int, InputType } from 'type-graphql';
2
- import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult } from '@memberjunction/core';
2
+ import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult, RunQueryEnrichment } from '@memberjunction/core';
3
3
  import { AppContext } from '../types.js';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { GraphQLJSONObject } from 'graphql-type-json';
@@ -40,6 +40,9 @@ export class RunQueryInput {
40
40
 
41
41
  @Field(() => String, { nullable: true, description: 'Description to use in audit log' })
42
42
  AuditLogDescription?: string;
43
+
44
+ @Field(() => GraphQLJSONObject, { nullable: true, description: 'Optional runtime-only directive to post-process result rows through a registered query result enricher ({ EnricherKey, Config })' })
45
+ Enrichment?: RunQueryEnrichment;
43
46
  }
44
47
 
45
48
  @ObjectType()
@@ -191,7 +194,8 @@ export class RunQueryResolver extends ResolverBase {
191
194
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
192
195
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
193
196
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
194
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
197
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
198
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
195
199
  // Check API key scope authorization for query execution
196
200
  await this.CheckAPIKeyScopeAuthorization('query:run', QueryID, context.userPayload);
197
201
 
@@ -200,7 +204,7 @@ export class RunQueryResolver extends ResolverBase {
200
204
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
201
205
  console.log('GetQueryData called with:', { QueryID, Parameters, MaxRows, StartRow, ForceAuditLog, AuditLogDescription });
202
206
  const result = await rq.RunQuery(
203
- {
207
+ {
204
208
  QueryID: QueryID,
205
209
  CategoryID: CategoryID,
206
210
  CategoryPath: CategoryPath,
@@ -208,8 +212,9 @@ export class RunQueryResolver extends ResolverBase {
208
212
  MaxRows: MaxRows,
209
213
  StartRow: StartRow,
210
214
  ForceAuditLog: ForceAuditLog,
211
- AuditLogDescription: AuditLogDescription
212
- },
215
+ AuditLogDescription: AuditLogDescription,
216
+ Enrichment: Enrichment
217
+ },
213
218
  context.userPayload.userRecord);
214
219
  console.log('RunQuery result:', {
215
220
  Success: result.Success,
@@ -256,22 +261,24 @@ export class RunQueryResolver extends ResolverBase {
256
261
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
257
262
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
258
263
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
259
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
264
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
265
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
260
266
  // Check API key scope authorization for query execution
261
267
  await this.CheckAPIKeyScopeAuthorization('query:run', QueryName, context.userPayload);
262
268
 
263
269
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
264
270
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
265
271
  const result = await rq.RunQuery(
266
- {
267
- QueryName: QueryName,
272
+ {
273
+ QueryName: QueryName,
268
274
  CategoryID: CategoryID,
269
275
  CategoryPath: CategoryPath,
270
276
  Parameters: Parameters,
271
277
  MaxRows: MaxRows,
272
278
  StartRow: StartRow,
273
279
  ForceAuditLog: ForceAuditLog,
274
- AuditLogDescription: AuditLogDescription
280
+ AuditLogDescription: AuditLogDescription,
281
+ Enrichment: Enrichment
275
282
  },
276
283
  context.userPayload.userRecord);
277
284
 
@@ -302,13 +309,14 @@ export class RunQueryResolver extends ResolverBase {
302
309
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
303
310
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
304
311
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
305
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
312
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
313
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
306
314
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
307
315
  const md = provider as unknown as IMetadataProvider;
308
316
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
309
317
 
310
318
  const result = await rq.RunQuery(
311
- {
319
+ {
312
320
  QueryID: QueryID,
313
321
  CategoryID: CategoryID,
314
322
  CategoryPath: CategoryPath,
@@ -316,8 +324,9 @@ export class RunQueryResolver extends ResolverBase {
316
324
  MaxRows: MaxRows,
317
325
  StartRow: StartRow,
318
326
  ForceAuditLog: ForceAuditLog,
319
- AuditLogDescription: AuditLogDescription
320
- },
327
+ AuditLogDescription: AuditLogDescription,
328
+ Enrichment: Enrichment
329
+ },
321
330
  context.userPayload.userRecord);
322
331
 
323
332
  // If QueryName is not populated by the provider, use efficient lookup
@@ -360,12 +369,13 @@ export class RunQueryResolver extends ResolverBase {
360
369
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
361
370
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
362
371
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
363
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
372
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
373
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
364
374
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
365
375
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
366
376
 
367
377
  const result = await rq.RunQuery(
368
- {
378
+ {
369
379
  QueryName: QueryName,
370
380
  CategoryID: CategoryID,
371
381
  CategoryPath: CategoryPath,
@@ -373,8 +383,9 @@ export class RunQueryResolver extends ResolverBase {
373
383
  MaxRows: MaxRows,
374
384
  StartRow: StartRow,
375
385
  ForceAuditLog: ForceAuditLog,
376
- AuditLogDescription: AuditLogDescription
377
- },
386
+ AuditLogDescription: AuditLogDescription,
387
+ Enrichment: Enrichment
388
+ },
378
389
  context.userPayload.userRecord);
379
390
 
380
391
  return {
@@ -420,7 +431,8 @@ export class RunQueryResolver extends ResolverBase {
420
431
  MaxRows: i.MaxRows,
421
432
  StartRow: i.StartRow,
422
433
  ForceAuditLog: i.ForceAuditLog,
423
- AuditLogDescription: i.AuditLogDescription
434
+ AuditLogDescription: i.AuditLogDescription,
435
+ Enrichment: i.Enrichment
424
436
  }));
425
437
 
426
438
  // Execute all queries in parallel using the batch method
@@ -469,7 +481,8 @@ export class RunQueryResolver extends ResolverBase {
469
481
  MaxRows: i.MaxRows,
470
482
  StartRow: i.StartRow,
471
483
  ForceAuditLog: i.ForceAuditLog,
472
- AuditLogDescription: i.AuditLogDescription
484
+ AuditLogDescription: i.AuditLogDescription,
485
+ Enrichment: i.Enrichment
473
486
  }));
474
487
 
475
488
  // Execute all queries in parallel using the batch method
@@ -532,6 +545,7 @@ export class RunQueryResolver extends ResolverBase {
532
545
  StartRow: item.params.StartRow,
533
546
  ForceAuditLog: item.params.ForceAuditLog,
534
547
  AuditLogDescription: item.params.AuditLogDescription,
548
+ Enrichment: item.params.Enrichment,
535
549
  },
536
550
  cacheStatus: item.cacheStatus ? {
537
551
  maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
@@ -1,14 +1,15 @@
1
1
  import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, InputType, Field } from 'type-graphql';
2
2
  import { randomUUID } from 'crypto';
3
- import { LogError, UserInfo, IMetadataProvider } from '@memberjunction/core';
3
+ import { LogError, LogStatusEx, UserInfo, IMetadataProvider } from '@memberjunction/core';
4
4
  import { LiveKitTokenService, LiveKitAgentRoomCoordinator, LiveKitEgressService } from '@memberjunction/livekit-room-server';
5
5
  import { AppContext } from '../types.js';
6
6
  import { ResolverBase } from '../generic/ResolverBase.js';
7
7
  import { GetReadWriteProvider } from '../util.js';
8
- import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink } from '@memberjunction/ai-agents';
8
+ import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink, RealtimeTurnModeratorDecision } from '@memberjunction/ai-agents';
9
9
  import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
10
10
  import { SessionManager } from '../agentSessions/SessionManager.js';
11
11
  import { NotificationEngine } from '@memberjunction/notifications';
12
+ import { registerMeetingRecordingFile, correlateRecordingStart } from './meetingRecordingRegistration.js';
12
13
 
13
14
  /**
14
15
  * Binds the agent realtime-session factory onto the LiveKit room coordinator's model-session creation seam.
@@ -36,9 +37,25 @@ AIBridgeEngine.Instance.SetSessionRunFinalizer(FinalizeBridgeCoAgentRuns);
36
37
  * "Meeting Room"/scope choices live HERE (the Meet composition layer), keeping the engine generic.
37
38
  */
38
39
  AIBridgeEngine.Instance.SetTranscriptSink(
39
- CreateBridgeRoomTranscriptSink({ ConversationType: 'Meeting Room', ApplicationScope: 'Application' }),
40
+ CreateBridgeRoomTranscriptSink({ ConversationType: 'Meeting Room', ApplicationScope: 'Application', ApplicationName: 'Meet' }),
40
41
  );
41
42
 
43
+ /**
44
+ * Binds the room **turn moderator** onto the bridge engine — **OPT-IN, off by default**. When
45
+ * `MJ_REALTIME_MODERATOR_MODE=on`, a multi-agent room routes each turn through a fast LLM prompt that decides
46
+ * who speaks (see `RealtimeTurnModerator` in `@memberjunction/ai-agents`). By default it's OFF: agents run in
47
+ * plain auto-response, hear everything, and self-moderate (no STT-driven router in the loop) — the
48
+ * coordinator likewise skips meeting mode when the flag is off, so the two stay consistent. We keep the
49
+ * moderator wired-but-toggleable for controlled scenarios (webinars, large rooms, weaker models).
50
+ */
51
+ if (process.env.MJ_REALTIME_MODERATOR_MODE === 'on') {
52
+ AIBridgeEngine.Instance.SetTurnModerator(RealtimeTurnModeratorDecision);
53
+ console.log('[RealtimeBridge] turn MODERATOR mode is ON (MJ_REALTIME_MODERATOR_MODE=on) — multi-agent rooms use the LLM router.');
54
+ } else {
55
+ // Default mode — only surface this at startup when verbose (MJ_VERBOSE) is on; it's the expected state and otherwise just noise.
56
+ LogStatusEx({ message: '[RealtimeBridge] turn moderator mode is OFF (default) — multi-agent rooms run free-for-all: all agents auto-respond + hear everything.', verboseOnly: true });
57
+ }
58
+
42
59
  /**
43
60
  * GraphQL surface for the MJ-native LiveKit room: mints scoped client access tokens and starts an
44
61
  * agent's presence in a room. The thin resolver delegates to `@memberjunction/livekit-room-server`
@@ -158,6 +175,10 @@ export class LiveKitRecordingResult {
158
175
 
159
176
  @Field(() => String)
160
177
  Status: string;
178
+
179
+ /** The `MJ: Files` row id of the registered recording (set on stop, once the egress MP4 is registered). */
180
+ @Field(() => String, { nullable: true })
181
+ RecordingFileID?: string;
161
182
  }
162
183
 
163
184
  /** A selectable provider-native voice for the dev voice picker. */
@@ -319,6 +340,33 @@ export class RealtimeBridgeResolver extends ResolverBase {
319
340
  }
320
341
  }
321
342
 
343
+ /**
344
+ * **Ends the meeting for everyone**: stops EVERY agent bot bridged into a room (by room name, via the
345
+ * coordinator's server-side roster). This is the "End meeting" half of the Zoom-style leave control —
346
+ * usable by any participant, including one who only *joined* the room and never tracked the bridge ids.
347
+ * Returns `true` when the teardown ran (even if the room held zero agents). Best-effort: any error → `false`.
348
+ *
349
+ * @param roomName The LiveKit room to end.
350
+ */
351
+ @Mutation(() => Boolean)
352
+ async EndLiveKitRoom(
353
+ @Arg('roomName', () => String) roomName: string,
354
+ @Ctx() context: AppContext = {} as AppContext,
355
+ ): Promise<boolean> {
356
+ try {
357
+ const user = this.GetUserFromPayload(context.userPayload);
358
+ if (!user) {
359
+ return false;
360
+ }
361
+ const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
362
+ await LiveKitAgentRoomCoordinator.Instance.StopAllAgentsInRoom(roomName, 'Explicit', user, provider);
363
+ return true;
364
+ } catch (error) {
365
+ LogError(`EndLiveKitRoom failed: ${error instanceof Error ? error.message : String(error)}`);
366
+ return false;
367
+ }
368
+ }
369
+
322
370
  /**
323
371
  * Lists active Realtime models with the voices each driver supports — the source for the dev model/voice
324
372
  * picker (gated client-side by the `Realtime: Advanced Session Controls` authorization). Read-only; returns
@@ -401,10 +449,17 @@ export class RealtimeBridgeResolver extends ResolverBase {
401
449
  @Ctx() context: AppContext = {} as AppContext,
402
450
  ): Promise<LiveKitRecordingResult> {
403
451
  try {
404
- if (!this.GetUserFromPayload(context.userPayload)) {
452
+ const user = this.GetUserFromPayload(context.userPayload);
453
+ if (!user) {
405
454
  return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: '', Status: '' };
406
455
  }
407
456
  const info = await new LiveKitEgressService().StartRoomRecording({ RoomName: input.RoomName, Layout: input.Layout });
457
+
458
+ // Best-effort: correlate the live recording with the room's Meeting-Room Conversation (if it exists
459
+ // yet) by stamping its EgressID. Never fail the start on this — the stop-flow resolves/creates it.
460
+ const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
461
+ void correlateRecordingStart(input.RoomName, info.EgressID, user, provider);
462
+
408
463
  return { Success: true, EgressID: info.EgressID, Status: info.Status };
409
464
  } catch (error) {
410
465
  const msg = error instanceof Error ? error.message : String(error);
@@ -413,15 +468,35 @@ export class RealtimeBridgeResolver extends ResolverBase {
413
468
  }
414
469
  }
415
470
 
416
- /** Stops a recording by egress id. */
471
+ /**
472
+ * Stops a recording by egress id, then REGISTERS the completed egress MP4 into MJStorage as an
473
+ * `MJ: Files` row linked to the room's Meeting-Room Conversation (`Conversation.RecordingFileID`).
474
+ * Registration is best-effort: a missing storage config or any failure returns the stop result with no
475
+ * `RecordingFileID` (the recording still stopped) — it never throws.
476
+ */
417
477
  @Mutation(() => LiveKitRecordingResult)
418
478
  async StopLiveKitRecording(@Arg('egressID', () => String) egressID: string, @Ctx() context: AppContext = {} as AppContext): Promise<LiveKitRecordingResult> {
419
479
  try {
420
- if (!this.GetUserFromPayload(context.userPayload)) {
480
+ const user = this.GetUserFromPayload(context.userPayload);
481
+ if (!user) {
421
482
  return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: egressID, Status: '' };
422
483
  }
423
484
  const info = await new LiveKitEgressService().StopRecording(egressID);
424
- return { Success: true, EgressID: info.EgressID, Status: info.Status };
485
+ const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
486
+
487
+ // Register the completed egress MP4 as a Files row on the Meeting-Room Conversation. Best-effort:
488
+ // any failure (e.g. storage provider not configured) leaves RecordingFileID unset but still
489
+ // returns the successful stop result.
490
+ const registration = await registerMeetingRecordingFile(
491
+ { EgressID: info.EgressID, RoomName: info.RoomName, OutputLocation: info.OutputLocation, OutputSizeBytes: info.OutputSizeBytes },
492
+ user,
493
+ provider,
494
+ );
495
+ if (!registration.Success) {
496
+ LogError(`StopLiveKitRecording: recording stopped but registration did not complete: ${registration.ErrorMessage ?? 'unknown'}`);
497
+ }
498
+
499
+ return { Success: true, EgressID: info.EgressID, Status: info.Status, RecordingFileID: registration.RecordingFileID };
425
500
  } catch (error) {
426
501
  const msg = error instanceof Error ? error.message : String(error);
427
502
  LogError(`StopLiveKitRecording failed: ${msg}`);