🐛 fix(server): stabilize heterogeneous main message chaining (#15783)

* ♻️ refactor(server): reduce main heterogeneous persistence

* 🐛 fix(server): anchor hetero turns to latest tool row
This commit is contained in:
Arvin Xu
2026-06-13 22:13:45 +08:00
committed by GitHub
parent f51dd06a36
commit 55a969a3c1
2 changed files with 456 additions and 661 deletions
File diff suppressed because it is too large Load Diff
@@ -486,9 +486,9 @@ describe('HeterogeneousPersistenceHandler', () => {
if (id === 'asst-1') order.push('update-asst'); if (id === 'asst-1') order.push('update-asst');
return origUpdate(id, patch); return origUpdate(id, patch);
}); });
h.messageModel.create.mockImplementation(async (input: any) => { h.messageModel.create.mockImplementation(async (input: any, id?: string) => {
order.push(input.role === 'tool' ? 'create-tool' : 'create-other'); order.push(input.role === 'tool' ? 'create-tool' : 'create-other');
return origCreate(input); return origCreate(input, id);
}); });
const tool = { const tool = {
@@ -767,6 +767,91 @@ describe('HeterogeneousPersistenceHandler', () => {
expect(step2Asst!.parentId).toBe('tool-row-only'); expect(step2Asst!.parentId).toBe('tool-row-only');
}); });
it('chains off the latest tool row when parallel tools are only partially backfilled', async () => {
// Regression for main-chain breaks with parallel/multi tool calls:
// tool A is visible in assistant.tools[].result_msg_id, while tool B's
// row exists but Phase 3 has not backfilled assistant.tools[] yet. The
// step anchor must be tool B, not the earlier resolved tool A.
const h = createHarness({
assistantMessageId: 'asst-init',
operationId: 'op-1',
topicId: 'topic-1',
});
const metaState: FakeTopicMetadata = {
runningOperation: { assistantMessageId: 'asst-init', operationId: 'op-1' },
};
h.topicModel.findById.mockImplementation(async (id: string) => {
if (id !== 'topic-1') return null;
return { agentId: null, id, metadata: { ...metaState } };
});
h.topicModel.updateMetadata.mockImplementation(async (_id: string, patch: any) => {
Object.assign(metaState, patch);
});
await h.handler.ingest({
events: [buildEvent('stream_start', 1, { newStep: true })],
operationId: 'op-1',
topicId: 'topic-1',
});
const step1Asst = [...h.messages.values()].find(
(m) => m.role === 'assistant' && m.id !== 'asst-init',
)!;
h.messages.set('tool-a-backfilled', {
agentId: null,
content: 'tool A result',
id: 'tool-a-backfilled',
parentId: step1Asst.id,
role: 'tool',
threadId: null,
tool_call_id: 'tc-a',
topicId: 'topic-1',
});
h.messages.set('tool-b-row-only', {
agentId: null,
content: 'tool B result',
id: 'tool-b-row-only',
parentId: step1Asst.id,
role: 'tool',
threadId: null,
tool_call_id: 'tc-b',
topicId: 'topic-1',
});
h.messages.set(step1Asst.id, {
...h.messages.get(step1Asst.id)!,
tools: [
{
apiName: 'Read',
arguments: '{}',
id: 'tc-a',
identifier: 'read',
result_msg_id: 'tool-a-backfilled',
type: 'default',
},
{
apiName: 'Bash',
arguments: '{}',
id: 'tc-b',
identifier: 'bash',
type: 'default',
},
],
});
await h.handler.ingest({
events: [buildEvent('stream_start', 2, { newStep: true })],
operationId: 'op-1',
topicId: 'topic-1',
});
const step2Asst = [...h.messages.values()].find(
(m) => m.role === 'assistant' && m.id !== 'asst-init' && m.id !== step1Asst.id,
);
expect(step2Asst).toBeDefined();
expect(step2Asst!.parentId).toBe('tool-b-row-only');
});
it('ignores subagent tool rows (threadId set) when resolving the step anchor', async () => { it('ignores subagent tool rows (threadId set) when resolving the step anchor', async () => {
// A subagent tool row lives on its own thread and must never anchor the // A subagent tool row lives on its own thread and must never anchor the
// main-agent wire. If the only `role:'tool'` child carries a threadId, // main-agent wire. If the only `role:'tool'` child carries a threadId,