From c3dce9221c1d8dc711203637a9822de5f16fdb48 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 6 Jan 2026 18:20:40 +0100
Subject: [PATCH] fixed ai looping jsonBase overwriting before getting
successful answer
---
modules/services/serviceAi/subAiCallLooping-flow.md | 6 ++++--
modules/services/serviceAi/subAiCallLooping.py | 10 ++++++++--
modules/shared/jsonContinuation.py | 5 +++--
3 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/modules/services/serviceAi/subAiCallLooping-flow.md b/modules/services/serviceAi/subAiCallLooping-flow.md
index e68b2f3c..0a7ac854 100644
--- a/modules/services/serviceAi/subAiCallLooping-flow.md
+++ b/modules/services/serviceAi/subAiCallLooping-flow.md
@@ -57,7 +57,7 @@ When an AI response is too large, it may be truncated (cut) at an arbitrary poin
**Function:** `self.aiService.callAi(request)`
- Returns `response.content` as `result`
-- Store raw response: `lastRawResponse = result`
+- NOTE: Do NOT update `lastRawResponse` yet! (only after successful merge)
### Step 4: MERGE
@@ -73,10 +73,12 @@ ELSE:
IF hasOverlap = False (MERGE FAILED):
→ mergeFailCount++
→ If mergeFailCount >= 3: return lastValidCompletePart (fallback)
- → Else: continue (retry with unchanged jsonBase)
+ → Else: continue (retry with unchanged jsonBase AND lastRawResponse!)
ELSE:
→ candidateJson = mergedJsonString (don't update jsonBase yet!)
+→ lastRawResponse = candidateJson (ONLY after first iteration or successful merge!)
+
TRY DIRECT PARSE of candidateJson:
IF parse succeeds:
→ jsonBase = candidateJson (commit)
diff --git a/modules/services/serviceAi/subAiCallLooping.py b/modules/services/serviceAi/subAiCallLooping.py
index f8eef9b9..6427b2e0 100644
--- a/modules/services/serviceAi/subAiCallLooping.py
+++ b/modules/services/serviceAi/subAiCallLooping.py
@@ -307,8 +307,9 @@ class AiCallLooper:
self.services.chat.progressLogFinish(iterationOperationId, True)
return result
- # Store raw response for continuation (even if broken)
- lastRawResponse = result
+ # NOTE: Do NOT update lastRawResponse here!
+ # lastRawResponse should only be updated after successful merge
+ # This ensures retry iterations use the correct base context
# Handle use cases that return JSON directly (no section extraction needed)
# Check if use case supports direct return (all registered use cases do)
@@ -387,6 +388,11 @@ class AiCallLooper:
candidateJson = mergedJsonString
logger.debug(f"Iteration {iteration}: Merge succeeded, candidateJson ({len(candidateJson)} chars)")
+ # Update lastRawResponse ONLY after we have a valid candidateJson
+ # (first iteration or successful merge - NOT on merge failure!)
+ # This ensures retry iterations use the correct base context
+ lastRawResponse = candidateJson
+
# Try direct parse of candidate
try:
extracted = extractJsonString(candidateJson)
diff --git a/modules/shared/jsonContinuation.py b/modules/shared/jsonContinuation.py
index 324b133d..a0ea2ea5 100644
--- a/modules/shared/jsonContinuation.py
+++ b/modules/shared/jsonContinuation.py
@@ -333,8 +333,9 @@ class JsonAnalyzer:
cutPos = len(self.jsonStr)
# Build both hierarchy contexts from the SAME structure BEFORE generating complete part
- # Generate hierarchy context WITHOUT budget (full structure for internal use)
- hierarchyContext = self._renderFromStructure(structure)
+ # CRITICAL: hierarchyContext must be the EXACT original JSON (for merge overlap detection!)
+ # The rendered version would have different formatting, breaking overlap matching
+ hierarchyContext = self.jsonStr
# Generate hierarchy context WITH budget (for prompts) - uses same structure
hierarchyContextForPrompt = self._renderWithBudgetFromStructure(structure, cutPos)