@studious-lms/server 1.5.0 → 1.7.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 (141) hide show
  1. package/dist/index.js +15 -3
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/config/cors.d.ts.map +1 -1
  4. package/dist/lib/config/cors.js +3 -2
  5. package/dist/lib/config/cors.js.map +1 -1
  6. package/dist/lib/pusher.d.ts +7 -1
  7. package/dist/lib/pusher.d.ts.map +1 -1
  8. package/dist/lib/pusher.js +9 -3
  9. package/dist/lib/pusher.js.map +1 -1
  10. package/dist/models/attendance.d.ts +16 -7
  11. package/dist/models/attendance.d.ts.map +1 -1
  12. package/dist/models/attendance.js +17 -2
  13. package/dist/models/attendance.js.map +1 -1
  14. package/dist/models/conversation.d.ts +33 -17
  15. package/dist/models/conversation.d.ts.map +1 -1
  16. package/dist/models/conversation.js +50 -2
  17. package/dist/models/conversation.js.map +1 -1
  18. package/dist/pipelines/aiLabChat.d.ts +20 -9
  19. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  20. package/dist/pipelines/aiLabChat.js +62 -74
  21. package/dist/pipelines/aiLabChat.js.map +1 -1
  22. package/dist/pipelines/aiNewtonChat.d.ts +13 -4
  23. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  24. package/dist/pipelines/aiNewtonChat.js +39 -27
  25. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  26. package/dist/pipelines/pdfToWorksheet.d.ts +31 -2
  27. package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
  28. package/dist/pipelines/pdfToWorksheet.js +160 -28
  29. package/dist/pipelines/pdfToWorksheet.js.map +1 -1
  30. package/dist/routers/_app.d.ts +48 -4
  31. package/dist/routers/_app.d.ts.map +1 -1
  32. package/dist/routers/assignment.d.ts.map +1 -1
  33. package/dist/routers/assignment.js +64 -4
  34. package/dist/routers/assignment.js.map +1 -1
  35. package/dist/routers/conversation.d.ts +15 -0
  36. package/dist/routers/conversation.d.ts.map +1 -1
  37. package/dist/routers/conversation.js +6 -3
  38. package/dist/routers/conversation.js.map +1 -1
  39. package/dist/routers/labChat.d.ts +1 -0
  40. package/dist/routers/labChat.d.ts.map +1 -1
  41. package/dist/routers/message.d.ts +3 -0
  42. package/dist/routers/message.d.ts.map +1 -1
  43. package/dist/routers/newtonChat.d.ts +1 -0
  44. package/dist/routers/newtonChat.d.ts.map +1 -1
  45. package/dist/routers/worksheet.d.ts +4 -2
  46. package/dist/routers/worksheet.d.ts.map +1 -1
  47. package/dist/routers/worksheet.js +16 -6
  48. package/dist/routers/worksheet.js.map +1 -1
  49. package/dist/services/announcement.js +4 -4
  50. package/dist/services/announcement.js.map +1 -1
  51. package/dist/services/assignment.d.ts.map +1 -1
  52. package/dist/services/assignment.js +16 -6
  53. package/dist/services/assignment.js.map +1 -1
  54. package/dist/services/attendance.d.ts.map +1 -1
  55. package/dist/services/attendance.js +68 -4
  56. package/dist/services/attendance.js.map +1 -1
  57. package/dist/services/conversation.d.ts +16 -0
  58. package/dist/services/conversation.d.ts.map +1 -1
  59. package/dist/services/conversation.js +22 -3
  60. package/dist/services/conversation.js.map +1 -1
  61. package/dist/services/labChat.d.ts +8 -1
  62. package/dist/services/labChat.d.ts.map +1 -1
  63. package/dist/services/labChat.js +101 -42
  64. package/dist/services/labChat.js.map +1 -1
  65. package/dist/services/message.d.ts +3 -0
  66. package/dist/services/message.d.ts.map +1 -1
  67. package/dist/services/message.js +8 -2
  68. package/dist/services/message.js.map +1 -1
  69. package/dist/services/newtonChat.d.ts +1 -0
  70. package/dist/services/newtonChat.d.ts.map +1 -1
  71. package/dist/services/newtonChat.js +40 -17
  72. package/dist/services/newtonChat.js.map +1 -1
  73. package/dist/services/worksheet.d.ts +15 -6
  74. package/dist/services/worksheet.d.ts.map +1 -1
  75. package/dist/services/worksheet.js +58 -12
  76. package/dist/services/worksheet.js.map +1 -1
  77. package/dist/utils/inference.d.ts +50 -1
  78. package/dist/utils/inference.d.ts.map +1 -1
  79. package/dist/utils/inference.js +167 -2
  80. package/dist/utils/inference.js.map +1 -1
  81. package/dist/workers/index.js +13 -3
  82. package/dist/workers/index.js.map +1 -1
  83. package/dist/workers/processors/aiLabChat.d.ts +7 -0
  84. package/dist/workers/processors/aiLabChat.d.ts.map +1 -0
  85. package/dist/workers/processors/aiLabChat.js +52 -0
  86. package/dist/workers/processors/aiLabChat.js.map +1 -0
  87. package/dist/workers/processors/aiNewtonChat.d.ts +7 -0
  88. package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -0
  89. package/dist/workers/processors/aiNewtonChat.js +57 -0
  90. package/dist/workers/processors/aiNewtonChat.js.map +1 -0
  91. package/dist/workers/processors/createRunJobWorker.d.ts +46 -0
  92. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -0
  93. package/dist/workers/processors/createRunJobWorker.js +118 -0
  94. package/dist/workers/processors/createRunJobWorker.js.map +1 -0
  95. package/dist/workers/processors/gradeWorksheet.d.ts +6 -0
  96. package/dist/workers/processors/gradeWorksheet.d.ts.map +1 -0
  97. package/dist/workers/processors/gradeWorksheet.js +19 -0
  98. package/dist/workers/processors/gradeWorksheet.js.map +1 -0
  99. package/dist/workers/processors/pdfToWorksheet.d.ts +5 -6
  100. package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -1
  101. package/dist/workers/processors/pdfToWorksheet.js +15 -95
  102. package/dist/workers/processors/pdfToWorksheet.js.map +1 -1
  103. package/dist/workers/processors/studentProgressChat.d.ts +8 -0
  104. package/dist/workers/processors/studentProgressChat.d.ts.map +1 -0
  105. package/dist/workers/processors/studentProgressChat.js +27 -0
  106. package/dist/workers/processors/studentProgressChat.js.map +1 -0
  107. package/dist/workers/queues.d.ts +20 -4
  108. package/dist/workers/queues.d.ts.map +1 -1
  109. package/dist/workers/queues.js +3 -3
  110. package/dist/workers/queues.js.map +1 -1
  111. package/package.json +3 -3
  112. package/src/index.ts +12 -1
  113. package/src/lib/config/cors.ts +1 -0
  114. package/src/lib/pusher.ts +7 -1
  115. package/src/models/attendance.ts +15 -0
  116. package/src/models/conversation.ts +54 -0
  117. package/src/pipelines/aiLabChat.ts +70 -78
  118. package/src/pipelines/aiNewtonChat.ts +47 -31
  119. package/src/pipelines/pdfToWorksheet.ts +224 -32
  120. package/src/routers/assignment.ts +74 -3
  121. package/src/routers/conversation.ts +7 -0
  122. package/src/routers/worksheet.ts +25 -4
  123. package/src/services/announcement.ts +2 -2
  124. package/src/services/assignment.ts +17 -4
  125. package/src/services/attendance.ts +87 -2
  126. package/src/services/conversation.ts +30 -0
  127. package/src/services/labChat.ts +106 -46
  128. package/src/services/message.ts +6 -0
  129. package/src/services/newtonChat.ts +45 -25
  130. package/src/services/worksheet.ts +71 -10
  131. package/src/utils/inference.ts +209 -1
  132. package/src/workers/index.ts +11 -1
  133. package/src/workers/processors/aiLabChat.ts +65 -0
  134. package/src/workers/processors/aiNewtonChat.ts +75 -0
  135. package/src/workers/processors/createRunJobWorker.ts +164 -0
  136. package/src/workers/processors/gradeWorksheet.ts +16 -0
  137. package/src/workers/processors/pdfToWorksheet.ts +12 -114
  138. package/src/workers/processors/studentProgressChat.ts +28 -0
  139. package/src/workers/queues.ts +20 -5
  140. package/tests/integration/routers/attendance.test.ts +60 -1
  141. package/tests/integration/routers/conversation.test.ts +121 -2
@@ -4,10 +4,13 @@
4
4
  */
5
5
  import { isAIUser } from "../utils/aiUser.js";
6
6
  import { prisma } from "../lib/prisma.js";
7
- import { JobStatus } from "@prisma/client";
8
7
  import { pusher, teacherChannel } from "../lib/pusher.js";
9
8
  import type { Assignment, Class, File, Section, User } from "@prisma/client";
10
- import { inference, sendAIMessage } from "../utils/inference.js";
9
+ import {
10
+ inference,
11
+ completePendingAIMessage,
12
+ failPendingAIMessage,
13
+ } from "../utils/inference.js";
11
14
  import { logger } from "../utils/logger.js";
12
15
  import { createPdf } from "../lib/jsonConversion.js";
13
16
  import { v4 } from "uuid";
@@ -328,52 +331,53 @@ export const getBaseSystemPrompt = (
328
331
 
329
332
 
330
333
  /**
331
- * Generate and send AI introduction for lab chat
332
- * Uses the stored context directly from database
334
+ * Fill the AI introduction placeholder for a brand-new lab chat.
335
+ *
336
+ * The caller (service layer) must have already created a PENDING AI Message
337
+ * row — `aiMessageId` is that placeholder. This function flips it to
338
+ * COMPLETED with the intro content, or to FAILED on a hard error.
333
339
  */
334
340
  export const generateAndSendLabIntroduction = async (
335
341
  labChatId: string,
336
342
  conversationId: string,
337
343
  _contextString: string,
338
- subject: string
344
+ _subject: string,
345
+ aiMessageId: string,
339
346
  ): Promise<void> => {
340
- try {
341
- const introMessage =
342
- "Hello teaching team! I'm your AI assistant for course material development. I'll help you create educational content - when you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?";
347
+ const introMessage =
348
+ "Hello teaching team! I'm your AI assistant for course material development. I'll help you create educational content - when you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?";
343
349
 
344
- await sendAIMessage(introMessage, conversationId, { subject });
345
-
346
- logger.info('AI Introduction sent', { labChatId, conversationId });
347
-
350
+ try {
351
+ await completePendingAIMessage(aiMessageId, introMessage);
352
+ logger.info('AI Introduction sent', { labChatId, conversationId, aiMessageId });
348
353
  } catch (error) {
349
- logger.error('Failed to generate AI introduction:', { error, labChatId });
350
-
351
- // Send fallback introduction
352
- try {
353
- const fallbackIntro = `Hello teaching team! I'm your AI assistant for course material development. I'll help you create educational content - when you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?`;
354
-
355
- await sendAIMessage(fallbackIntro, conversationId, {
356
- subject,
357
- });
358
-
359
- logger.info('Fallback AI introduction sent', { labChatId });
360
-
361
- } catch (fallbackError) {
362
- logger.error('Failed to send fallback AI introduction:', { error: fallbackError, labChatId });
363
- }
354
+ logger.error('Failed to complete AI introduction placeholder:', {
355
+ error,
356
+ labChatId,
357
+ aiMessageId,
358
+ });
359
+ await failPendingAIMessage(aiMessageId, { reason: 'lab_intro_complete_failed' });
364
360
  }
365
361
  }
366
362
 
367
363
  /**
368
- * Generate and send AI response to teacher message
369
- * Uses the stored context directly from database
370
- * @param emitOptions - When provided, emits lab-response-completed/failed on teacher channel
371
- * `_teacherMessage` is retained for caller compatibility while generation is anchored by `emitOptions.messageId`.
364
+ * Fill the AI response placeholder for a teacher reply in a lab chat.
365
+ *
366
+ * `aiMessageId` is the PENDING AI Message row created by the service
367
+ * layer; the pipeline flips it to COMPLETED with content / attachments /
368
+ * meta, or FAILED on error. Teacher-channel `lab-response-{pending,
369
+ * completed,failed}` events fire alongside the chat-channel status flips
370
+ * (which `completePendingAIMessage` / `failPendingAIMessage` already
371
+ * broadcast) so the lab list "AI busy" indicator stays accurate.
372
+ *
373
+ * `_teacherMessage` is retained for caller compatibility — the actual
374
+ * conversation history is reloaded from the DB anchored on the AI
375
+ * placeholder.
372
376
  */
373
377
  export const generateAndSendLabResponse = async (
374
378
  labChatId: string,
375
379
  _teacherMessage: string,
376
- emitOptions?: { classId: string; messageId: string }
380
+ emitOptions: { classId: string; aiMessageId: string }
377
381
  ): Promise<void> => {
378
382
  try {
379
383
  // Get lab context from database
@@ -395,9 +399,15 @@ export const generateAndSendLabIntroduction = async (
395
399
 
396
400
  const conversationId = fullLabChat.conversationId;
397
401
 
398
- const recentMessages = await loadRecentLabChatMessages(
402
+ // Anchor history on the AI placeholder — the slice will include the
403
+ // placeholder itself (empty content) at the top; drop it so the model
404
+ // does not see an empty assistant turn at the very end.
405
+ const anchoredMessages = await loadRecentLabChatMessages(
399
406
  conversationId,
400
- emitOptions?.messageId,
407
+ emitOptions.aiMessageId,
408
+ );
409
+ const recentMessages = anchoredMessages.filter(
410
+ (msg) => msg.id !== emitOptions.aiMessageId,
401
411
  );
402
412
 
403
413
  const classData = await prisma.class.findUnique({
@@ -486,7 +496,7 @@ export const generateAndSendLabIntroduction = async (
486
496
  labChatId,
487
497
  conversationId,
488
498
  classId: fullLabChat.classId,
489
- ...(emitOptions?.messageId ? { messageId: emitOptions.messageId } : {}),
499
+ aiMessageId: emitOptions.aiMessageId,
490
500
  },
491
501
  }),
492
502
  LAB_CHAT_RESPONSE_TIMEOUT_MS,
@@ -567,8 +577,7 @@ export const generateAndSendLabIntroduction = async (
567
577
  }
568
578
  }
569
579
 
570
- // Send the text response to the conversation
571
- await sendAIMessage(jsonData.text, conversationId, {
580
+ await completePendingAIMessage(emitOptions.aiMessageId, jsonData.text, {
572
581
  attachments: {
573
582
  connect: attachmentIds.map(id => ({ id })),
574
583
  },
@@ -586,32 +595,24 @@ export const generateAndSendLabIntroduction = async (
586
595
  id: v4(),
587
596
  })) || null,
588
597
  },
589
- subject: fullLabChat.class?.subject || 'Lab',
590
598
  });
591
599
  } catch (parseError) {
592
600
  logger.error('Failed to parse AI response or generate PDF:', { error: parseError, labChatId });
593
- // Fallback: send the raw response if parsing fails
594
- await sendAIMessage(response.text, conversationId, {
595
- subject: fullLabChat.class?.subject || 'Lab',
596
- });
601
+ // Fallback: complete the placeholder with the raw text so the
602
+ // teacher still sees something useful instead of FAILED.
603
+ await completePendingAIMessage(emitOptions.aiMessageId, response.text);
597
604
  }
598
605
 
599
- if (emitOptions) {
600
- await prisma.message.update({
601
- where: { id: emitOptions.messageId },
602
- data: { status: JobStatus.COMPLETED },
606
+ try {
607
+ await pusher.trigger(teacherChannel(emitOptions.classId), "lab-response-completed", {
608
+ labChatId,
609
+ messageId: emitOptions.aiMessageId,
603
610
  });
604
- try {
605
- await pusher.trigger(teacherChannel(emitOptions.classId), "lab-response-completed", {
606
- labChatId,
607
- messageId: emitOptions.messageId,
608
- });
609
- } catch (broadcastError) {
610
- logger.error("Failed to broadcast lab response completed:", { error: broadcastError });
611
- }
611
+ } catch (broadcastError) {
612
+ logger.error("Failed to broadcast lab response completed:", { error: broadcastError });
612
613
  }
613
614
 
614
- logger.info('AI response sent', { labChatId, conversationId });
615
+ logger.info('AI response sent', { labChatId, conversationId, aiMessageId: emitOptions.aiMessageId });
615
616
 
616
617
  } catch (error) {
617
618
  console.error('Full error object:', error);
@@ -621,31 +622,22 @@ export const generateAndSendLabIntroduction = async (
621
622
  stack: error.stack,
622
623
  name: error.name
623
624
  } : error,
624
- labChatId
625
+ labChatId,
626
+ aiMessageId: emitOptions.aiMessageId,
625
627
  });
626
628
 
627
- if (emitOptions) {
628
- try {
629
- await prisma.message.update({
630
- where: { id: emitOptions.messageId },
631
- data: { status: JobStatus.FAILED },
632
- });
633
- } catch (statusError) {
634
- logger.error("Failed to set message status FAILED:", {
635
- error: statusError,
636
- labChatId,
637
- messageId: emitOptions.messageId,
638
- });
639
- }
640
- try {
641
- await pusher.trigger(teacherChannel(emitOptions.classId), "lab-response-failed", {
642
- labChatId,
643
- messageId: emitOptions.messageId,
644
- error: "AI response generation failed",
645
- });
646
- } catch (broadcastError) {
647
- logger.error("Failed to broadcast lab response failed:", { error: broadcastError });
648
- }
629
+ await failPendingAIMessage(emitOptions.aiMessageId, {
630
+ reason: 'lab_response_generation_failed',
631
+ });
632
+
633
+ try {
634
+ await pusher.trigger(teacherChannel(emitOptions.classId), "lab-response-failed", {
635
+ labChatId,
636
+ messageId: emitOptions.aiMessageId,
637
+ error: "AI response generation failed",
638
+ });
639
+ } catch (broadcastError) {
640
+ logger.error("Failed to broadcast lab response failed:", { error: broadcastError });
649
641
  }
650
642
 
651
643
  throw error; // Re-throw to see the full error in the calling function
@@ -3,11 +3,13 @@
3
3
  * Uses assignment context and AI policy levels for tutoring.
4
4
  */
5
5
  import { prisma } from "../lib/prisma.js";
6
- import { inference } from "../utils/inference.js";
6
+ import {
7
+ inference,
8
+ completePendingAIMessage,
9
+ failPendingAIMessage,
10
+ } from "../utils/inference.js";
7
11
  import { logger } from "../utils/logger.js";
8
- import { sendAIMessage } from "../utils/inference.js";
9
12
  import { isAIUser } from "../utils/aiUser.js";
10
- import { Assignment } from "@prisma/client";
11
13
 
12
14
 
13
15
  // AI Policy Levels Configuration
@@ -79,12 +81,18 @@ export interface AIPolicyLevel {
79
81
  ];
80
82
 
81
83
  /**
82
- * Generate and send AI introduction for Newton chat
84
+ * Fill the Newton tutor introduction placeholder.
85
+ *
86
+ * `aiMessageId` is the PENDING AI Message row created by the service layer
87
+ * (see `getTutorConversation`). On success we flip it to COMPLETED with the
88
+ * generated intro; on failure we fall back to a hardcoded greeting before
89
+ * marking the placeholder FAILED only as a last resort.
83
90
  */
84
91
  export const generateAndSendNewtonIntroduction = async (
85
92
  newtonChatId: string,
86
93
  conversationId: string,
87
- submissionId: string
94
+ submissionId: string,
95
+ aiMessageId: string,
88
96
  ): Promise<void> => {
89
97
  try {
90
98
  // Get submission details for context
@@ -144,6 +152,7 @@ export const generateAndSendNewtonIntroduction = async (
144
152
  newtonChatId,
145
153
  conversationId,
146
154
  submissionId: submission.id,
155
+ aiMessageId,
147
156
  },
148
157
  model: 'command-a-03-2025',
149
158
  maxTokens: 300,
@@ -154,28 +163,22 @@ export const generateAndSendNewtonIntroduction = async (
154
163
  throw new Error('No response generated from inference API');
155
164
  }
156
165
 
157
- // Send AI introduction using centralized sender
158
- await sendAIMessage(response, conversationId, {
159
- subject: submission.assignment.class.subject || 'Assignment',
160
- });
166
+ await completePendingAIMessage(aiMessageId, response);
161
167
 
162
- logger.info('AI Introduction sent', { newtonChatId, conversationId });
168
+ logger.info('AI Introduction sent', { newtonChatId, conversationId, aiMessageId });
163
169
 
164
170
  } catch (error) {
165
- logger.error('Failed to generate AI introduction:', { error, newtonChatId });
171
+ logger.error('Failed to generate AI introduction:', { error, newtonChatId, aiMessageId });
166
172
 
167
- // Send fallback introduction
173
+ // Fallback: still complete the placeholder so the student isn't stuck
174
+ // staring at an empty bubble.
168
175
  try {
169
176
  const fallbackIntro = `Hello! I'm Newton, your AI tutor. I'm here to help you with your assignment. I can answer questions, explain concepts, and guide you through your work. What would you like help with today?`;
170
-
171
- await sendAIMessage(fallbackIntro, conversationId, {
172
- subject: 'Assignment',
173
- });
174
-
175
- logger.info('Fallback AI introduction sent', { newtonChatId });
176
-
177
+ await completePendingAIMessage(aiMessageId, fallbackIntro);
178
+ logger.info('Fallback AI introduction sent', { newtonChatId, aiMessageId });
177
179
  } catch (fallbackError) {
178
- logger.error('Failed to send fallback AI introduction:', { error: fallbackError, newtonChatId });
180
+ logger.error('Failed to send fallback AI introduction:', { error: fallbackError, newtonChatId, aiMessageId });
181
+ await failPendingAIMessage(aiMessageId, { reason: 'newton_intro_fallback_failed' });
179
182
  }
180
183
  }
181
184
  }
@@ -209,7 +212,11 @@ export const generateAndSendNewtonIntroduction = async (
209
212
  };
210
213
 
211
214
  /**
212
- * Generate and send AI response to student message
215
+ * Fill the Newton AI response placeholder for a student turn.
216
+ *
217
+ * `aiMessageId` is the PENDING AI Message row the service created post-tx;
218
+ * the pipeline flips it to COMPLETED with the generated content (the
219
+ * helper broadcasts `message-updated` on the chat channel) or to FAILED.
213
220
  */
214
221
  export const generateAndSendNewtonResponse = async (
215
222
  newtonChatId: string,
@@ -225,13 +232,17 @@ export const generateAndSendNewtonIntroduction = async (
225
232
  subject: string | null;
226
233
  };
227
234
  };
228
- }
235
+ },
236
+ aiMessageId: string,
229
237
  ): Promise<void> => {
230
238
  try {
231
- // Get recent conversation history
239
+ // Get recent conversation history (excluding the empty AI placeholder
240
+ // we just created — otherwise the model sees a blank assistant turn at
241
+ // the very end of the transcript).
232
242
  const recentMessages = await prisma.message.findMany({
233
243
  where: {
234
244
  conversationId,
245
+ id: { not: aiMessageId },
235
246
  },
236
247
  include: {
237
248
  sender: {
@@ -329,6 +340,7 @@ export const generateAndSendNewtonIntroduction = async (
329
340
  newtonChatId,
330
341
  conversationId,
331
342
  submissionId: submission.id,
343
+ aiMessageId,
332
344
  },
333
345
  });
334
346
 
@@ -336,13 +348,10 @@ export const generateAndSendNewtonIntroduction = async (
336
348
  throw new Error('No response generated from inference API');
337
349
  }
338
350
 
339
- // Send the text response to the conversation
340
- await sendAIMessage(response, conversationId, {
341
- subject: submission.assignment.class.subject || 'Assignment',
342
- });
343
-
344
- logger.info('AI response sent', { newtonChatId, conversationId });
345
-
351
+ await completePendingAIMessage(aiMessageId, response);
352
+
353
+ logger.info('AI response sent', { newtonChatId, conversationId, aiMessageId });
354
+
346
355
  } catch (error) {
347
356
  logger.error('Failed to generate AI response:', {
348
357
  error: error instanceof Error ? {
@@ -350,8 +359,15 @@ export const generateAndSendNewtonIntroduction = async (
350
359
  stack: error.stack,
351
360
  name: error.name
352
361
  } : error,
353
- newtonChatId
362
+ newtonChatId,
363
+ aiMessageId,
354
364
  });
365
+
366
+ await failPendingAIMessage(aiMessageId, {
367
+ reason: 'newton_response_generation_failed',
368
+ });
369
+
370
+ throw error;
355
371
  }
356
372
  }
357
373