{"record":{"id":"b28cc5a2e422d3b3","repo":"alibaba/spring-ai-alibaba","slug":"mergeall-can-only-be-called-once","errorCode":null,"errorMessage":"mergeAll() can only be called once","messagePattern":"mergeAll\\(\\) can only be called once","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/tool/ToolStateCollector.java","lineNumber":131,"sourceCode":"\tpublic void discardToolUpdateMap(int index) {\n\t\ttoolUpdatesByIndex.remove(index);\n\t}\n\n\t/**\n\t * Merges all tool updates in original index order (0, 1, 2, ...). This ensures\n\t * deterministic results regardless of completion order.\n\t *\n\t * <p>\n\t * <b>Note:</b> This method can only be called once. Subsequent calls will throw an\n\t * {@link IllegalStateException}. This ensures that merging happens only after all\n\t * tools have completed their execution.\n\t * </p>\n\t * @return the merged state updates\n\t * @throws IllegalStateException if called more than once\n\t */\n\tpublic Map<String, Object> mergeAll() {\n\t\tif (!merged.compareAndSet(false, true)) {\n\t\t\tthrow new IllegalStateException(\"mergeAll() can only be called once\");\n\t\t}\n\n\t\tMap<String, Object> result = new ConcurrentHashMap<>();\n\n\t\tfor (int i = 0; i < totalTools; i++) {\n\t\t\tMap<String, Object> toolUpdate = toolUpdatesByIndex.get(i);\n\t\t\tif (toolUpdate == null || toolUpdate.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (Map.Entry<String, Object> entry : toolUpdate.entrySet()) {\n\t\t\t\tString key = entry.getKey();\n\t\t\t\tObject newValue = entry.getValue();\n\t\t\t\tObject existingValue = result.get(key);\n\n\t\t\t\tif (existingValue == null) {\n\t\t\t\t\tresult.put(key, newValue);\n\t\t\t\t}","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/tool/ToolStateCollector.java#L113-L149","documentation":"ToolStateCollector.mergeAll() is one-shot: it uses merged.compareAndSet(false, true) and throws IllegalStateException \"mergeAll() can only be called once\" on any second invocation. This prevents double-merging tool updates into agent state.","triggerScenarios":"Calling mergeAll() twice on the same collector instance — e.g. retry logic calling it again after a partial failure, or code paths that both finalize state and re-merge.","commonSituations":"Retry wrappers around tool execution, accidental double invocation in orchestration code, tests that call mergeAll per assertion.","solutions":["Call mergeAll() exactly once, store and reuse the returned map.","Create a new ToolStateCollector for each batch of tool calls.","Wrap mergeAll() in an idempotent helper that caches the result.","Remove retry logic that re-invokes mergeAll; retry the tools instead on a fresh collector."],"exampleFix":"// before\nvar r1 = collector.mergeAll();\nvar r2 = collector.mergeAll(); // throws\n// after\nvar r1 = collector.mergeAll();\n// reuse r1 instead of merging again","handlingStrategy":"validation","validationCode":"// idempotent wrapper\nMap<String,Object> merged; boolean done;\nMap<String,Object> mergeOnce(ToolStateCollector c) { return done ? merged : (merged = c.mergeAll()); }","typeGuard":null,"tryCatchPattern":"try { return collector.mergeAll(); } catch (IllegalStateException e) { return previouslyMerged; }","preventionTips":["Call mergeAll exactly once and cache the result","Never retry mergeAll; retry the tool round instead","Use fresh collectors per batch","Keep merge logic in one orchestration method"],"tags":["state","lifecycle","illegal-state","one-shot"],"backgroundTag":"invalid-state-transition","analyzedSha":"f82da0b50f35744c13968191be2b1cd2452ef550","analyzedAt":"2026-09-09T15:32:42.421Z","contentChangedAt":"2026-09-09T15:32:42.421Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}