Ensure unique fallback tool ids

This commit is contained in:
Mohamed Boudra
2025-11-15 17:41:15 +01:00
parent 19d172cc8d
commit e208f1c5bd
3 changed files with 43 additions and 3 deletions

View File

@@ -1199,6 +1199,43 @@ function testMetadataReplayDeduplicationHydrated() {
});
}
function testFallbackToolCallIdsStayUnique() {
const timestamp = new Date('2025-01-01T14:05:00Z');
const updates = [
{
event: toolTimeline(
'fallback-shell-1',
'completed',
{ type: 'tool_result', tool_use_id: 'fallback-shell-1' },
{ callId: null, server: 'command', tool: 'shell', displayName: 'Run shell' }
),
timestamp,
},
{
event: toolTimeline(
'fallback-shell-2',
'completed',
{ type: 'tool_result', tool_use_id: 'fallback-shell-2' },
{ callId: null, server: 'command', tool: 'shell', displayName: 'Run shell' }
),
timestamp,
},
];
const hydrated = hydrateStreamState(updates);
const toolCalls = hydrated.filter(
(item): item is ToolCallItem => item.kind === 'tool_call' && item.payload.source === 'agent'
);
assert.strictEqual(toolCalls.length, 2, 'Hydration should retain multiple fallback tool entries');
const ids = toolCalls.map((item) => item.id);
assert.strictEqual(
new Set(ids).size,
ids.length,
'Fallback-generated tool ids must be unique even when metadata matches and callIds are missing'
);
}
describe('stream timeline reducers', () => {
it('produces deterministic hydration results', testIdempotentReduction);
it('deduplicates pending/completed tool entries in place', testUserMessageDeduplication);
@@ -1222,4 +1259,5 @@ describe('stream timeline reducers', () => {
it('merges out-of-order tool call updates without duplicating entries (hydrated)', testOutOfOrderToolCallMergingHydrated);
it('replays metadata-only tool calls without duplicating entries (live)', testMetadataReplayDeduplicationLive);
it('replays metadata-only tool calls without duplicating entries (hydrated)', testMetadataReplayDeduplicationHydrated);
it('assigns unique ids for fallback tool calls without call ids', testFallbackToolCallIdsStayUnique);
});

View File

@@ -35,7 +35,7 @@ function createTimelineId(prefix: string, text: string, timestamp: Date): string
function createUniqueTimelineId(
state: StreamItem[],
prefix: "assistant" | "thought" | "user",
prefix: "assistant" | "thought" | "user" | "tool",
text: string,
timestamp: Date
): string {
@@ -459,7 +459,8 @@ function appendAgentToolCall(
const id = callId
? `agent_tool_${callId}`
: createTimelineId(
: createUniqueTimelineId(
state,
"tool",
`${data.provider}:${data.server}:${data.tool}`,
timestamp

View File

@@ -26,7 +26,8 @@
- Added a dedicated `useImageAttachmentPicker` hook that registers the media picker launcher up front, reuses Expos permission hook, restores pending results, and guards against concurrent launches. AgentInput now consumes this hook so tapping the attachment button opens the picker without crashing on Android; `npm run typecheck --workspace=@voice-dev/app` currently fails upstream in `stream.test.ts` before our change, noted in the summary.
- [x] Do not mark the Claude hydration fix complete until theres an end-to-end test that (1) runs Claude, (2) executes tool calls, (3) persists the conversation under `~/.claude/projects/...`, and (4) hydrates from disk verifying the tool call results render. No test, no checkbox.
- Strengthened `packages/server/src/server/agent/providers/claude-agent.test.ts` so the existing e2e hydration suite now asserts the live stream parses command/edit/read payloads and that the resumed stream reproduces those parsed diffs/reads/command outputs from `~/.claude/projects`, guaranteeing the UI pills render identical results after hydration.
- [ ] ERROR Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. .$tool_1763217454926_uyr9rm
- [x] ERROR Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. .$tool_1763217454926_uyr9rm
- `Stream` now generates fallback tool ids via `createUniqueTimelineId`, ensuring hydrated/past tool calls without callIds never reuse the same key even when their metadata matches; added `testFallbackToolCallIdsStayUnique` in `packages/app/src/types/stream.test.ts` to cover the regression and verified via `npx vitest packages/app/src/types/stream.test.ts`.
```tsx
Code: agent-stream-view.tsx