Browse Source

fix: implement loadMessagesWithRetry to prevent agent thinking UI stuck state during message fetch latency

bobbyunknown 1 month ago
parent
commit
845809f44e
2 changed files with 44 additions and 2 deletions
  1. 26 1
      src/interview/parser.ts
  2. 18 1
      src/interview/service.ts

+ 26 - 1
src/interview/parser.ts

@@ -81,8 +81,33 @@ export function parseAssistantState(
     return { state: null };
   }
 
+  // Pre-process match[1] to repair common JSON escaping issues (e.g. unescaped newlines inside strings)
+  let rawJson = match[1].trim();
+
+  // A robust heuristic to escape literal carriage returns/newlines inside JSON string values
+  // so JSON.parse doesn't throw "JSON Parse error: Expected '}'" or "Unexpected token".
+  // This is safe because it only targets characters within quotes.
+  try {
+    // If it parses directly, great!
+    JSON.parse(rawJson);
+  } catch {
+    // Try to normalize literal newlines inside string values:
+    rawJson = rawJson.replace(
+      /:[ \t]*"([\s\S]*?)"([ \t]*[,}])/g,
+      (_m, content, suffix) => {
+        // Escape real newlines and backslash escapes in content
+        const escaped = content
+          .replace(/\\/g, '\\\\')
+          .replace(/\n/g, '\\n')
+          .replace(/\r/g, '\\r')
+          .replace(/"/g, '\\"');
+        return `: "${escaped}"${suffix}`;
+      },
+    );
+  }
+
   try {
-    const raw = JSON.parse(match[1]);
+    const raw = JSON.parse(rawJson);
     // Validate raw LLM output with Zod before processing
     const parsed = RawInterviewStateSchema.parse(raw) as Record<
       string,

+ 18 - 1
src/interview/service.ts

@@ -259,6 +259,23 @@ export function createInterviewService(
     return result.data as InterviewMessage[];
   }
 
+  async function loadMessagesWithRetry(
+    sessionID: string,
+  ): Promise<InterviewMessage[]> {
+    const _lastLength = 0;
+    for (let i = 0; i < 8; i++) {
+      const messages = await loadMessages(sessionID);
+      if (messages.length > 0) {
+        const last = messages[messages.length - 1];
+        if (last?.info?.role === 'assistant') {
+          return messages;
+        }
+      }
+      await new Promise((resolve) => setTimeout(resolve, 250));
+    }
+    return loadMessages(sessionID);
+  }
+
   function isUserVisibleMessage(message: InterviewMessage): boolean {
     return !(message.parts ?? []).some((part) =>
       hasInternalInitiatorMarker(part),
@@ -350,7 +367,7 @@ export function createInterviewService(
   async function syncInterview(
     interview: InterviewRecord,
   ): Promise<InterviewState> {
-    const allMessages = await loadMessages(interview.sessionID);
+    const allMessages = await loadMessagesWithRetry(interview.sessionID);
     const interviewMessages = allMessages
       .slice(interview.baseMessageCount)
       .filter(isUserVisibleMessage);