fix: address tool_result dedup, SDK task ID collision, and doc accuracy

P1: tool_result dedup changed from "first wins" to "last wins"
- claude-client.ts: remove emittedToolResultIds Set, both PostToolUse and
  user message handler emit freely. PostToolUse provides immediate UI feedback;
  user handler's potentially more complete result arrives second.
- route.ts: collectStreamResponse replaces existing contentBlock entry when
  duplicate tool_use_id arrives (last wins for DB persistence).
- ChatView.tsx: onToolResult replaces existing state entry by tool_use_id
  instead of skipping, ensuring most complete result is displayed.

P2: SDK task ID collision risk eliminated
- db.ts: syncSdkTasks now uses full sessionId in task ID
  (`sdk-{fullSessionId}-{index}`) instead of truncated 8-char prefix,
  eliminating cross-session primary key collisions.

P2: Todo ordering stabilized
- db.ts: add sort_order column migration, syncSdkTasks writes array index
  as sort_order. getTasksBySession sorts by sort_order ASC, created_at ASC
  so tasks with identical timestamps maintain insertion order.

P3: Documentation field mapping corrected
- agent-tooling-todo-bridge.md: fix field mapping table — actual implementation
  maps activeForm (not priority) to description column. Updated dedup strategy
  docs from "three-layer skip" to "two-layer last-wins replace".

Lint: fix prefer-const for finalPrompt in claude-client.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-27 13:22:10 +08:00
1 parent c13ff54638
commit 0c379e5607
5 files changed
+64 -54

No files matched your search

+17 -8
View File
@@ -272,15 +272,24 @@ async function collectStreamResponse(stream: ReadableStream<string>, sessionId:
} else if (event.type === 'tool_result') {
try {
const resultData = JSON.parse(event.data);
// Dedup: skip if already seen
if (!seenToolResultIds.has(resultData.tool_use_id)) {
const newBlock = {
type: 'tool_result' as const,
tool_use_id: resultData.tool_use_id,
content: resultData.content,
is_error: resultData.is_error || false,
};
// Last-wins: if same tool_use_id already exists, replace it
// (user handler's result may be more complete than PostToolUse's)
if (seenToolResultIds.has(resultData.tool_use_id)) {
const idx = contentBlocks.findIndex(
(b) => b.type === 'tool_result' && 'tool_use_id' in b && b.tool_use_id === resultData.tool_use_id
);
if (idx >= 0) {
contentBlocks[idx] = newBlock;
}
} else {
seenToolResultIds.add(resultData.tool_use_id);
contentBlocks.push({
type: 'tool_result',
tool_use_id: resultData.tool_use_id,
content: resultData.content,
is_error: resultData.is_error || false,
});
contentBlocks.push(newBlock);
}
} catch {
// skip malformed tool_result data