fix(ui): stabilize company chat result topology

This commit is contained in:
LZH-YS1998
2026-07-14 20:31:10 +08:00
parent 0a6246aff9
commit 5938b4c215
25 changed files with 1917 additions and 381 deletions
@@ -241,6 +241,153 @@ assert.equal(repeatedNativeCompanySync.length, 1)
assert.equal(repeatedNativeCompanySync[0].metadata?.ui_timeline_id, 'message:native-raw-1')
assert.equal(repeatedNativeCompanySync[0].timestamp, 5)
const structuredDeliveryMerge = __chatStoreTestUtils.dedupeMessages([
{
...nativeCompanyRawTurn,
content: 'Raw runtime wording before the canonical result wrapper.',
metadata: {
...nativeCompanyRawTurn.metadata,
result_delivery_id: 'result:company-turn-1:attempt:0',
},
},
{
...companyRoleResult,
content: 'Canonical committed wording may differ without creating another row.',
metadata: {
...companyRoleResult.metadata,
result_delivery_id: 'result:company-turn-1:attempt:0',
},
},
])
assert.equal(structuredDeliveryMerge.length, 1)
assert.equal(structuredDeliveryMerge[0].id, 'role-result-1')
assert.equal(
structuredDeliveryMerge[0].content,
'Canonical committed wording may differ without creating another row.',
)
const deliveryMirrorWithLongerWrapper = __chatStoreTestUtils.dedupeMessages([
{
...nativeCompanyRawTurn,
content: 'Runtime wrapper that must not outrank the committed surface.\n\nCanonical result body.',
metadata: {
...nativeCompanyRawTurn.metadata,
result_delivery_id: 'result:company-turn-wrapper:attempt:0',
},
},
{
...companyRoleResult,
content: 'Canonical result body.',
metadata: {
...companyRoleResult.metadata,
result_delivery_id: 'result:company-turn-wrapper:attempt:0',
},
},
])
assert.equal(deliveryMirrorWithLongerWrapper.length, 1)
assert.equal(
deliveryMirrorWithLongerWrapper[0].content,
'Canonical result body.',
'delivery mirrors follow surface authority; suffix repair only applies to one persistent message id',
)
const ctoDispatchContent = `Both work items have been successfully dispatched to my senior engineer. Here's the status:
## Dispatch Summary
**Work Item 1: OpenOPC Source Code Architecture Deep-Dive Analysis**
- ID: \`1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82\`
- Owner: senior_engineer
- Scope: \`openopc-source-analysis\`
- Output: \`/data2/bjdwhzzh/project-hku/OpenOPC_workplace/0009/openopc-architecture-analysis.md\`
- Covers: Layered architecture, the work-item state machine, collaboration policy, and seat executor mechanisms.
**Work Item 2: External Multi-Agent Frameworks Architecture Research**
- ID: \`d0307208-6b95-44c1-9b51-6bf073bbdcef\`
- Owner: senior_engineer
Both work items are independent and can execute in parallel.`
const ctoCompanyFinal: ChatMessage = {
id: 'runtime-v2-company-assistant-final:cto-turn-9',
channelId: 'session:cto-work-item',
sender: 'cto',
senderName: 'CTO',
content: ctoDispatchContent,
timestamp: 20,
mentions: [],
metadata: {
source: 'engine',
transcript_kind: 'runtime_v2_company_assistant',
canonical_turn_id: 'cto-turn-9',
ui_message_id: 'runtime-v2-company-assistant-final:cto-turn-9',
},
}
const repeatedCtoSnapshot = __chatStoreTestUtils.dedupeMessages([
ctoCompanyFinal,
{ ...ctoCompanyFinal, metadata: { ...ctoCompanyFinal.metadata } },
])
assert.equal(repeatedCtoSnapshot.length, 1)
assert.equal(
repeatedCtoSnapshot[0].content,
ctoDispatchContent,
'comparison normalization must never peel Work Item, ID, or Owner fields from rendered content',
)
let repeatedCtoMerge = [ctoCompanyFinal]
for (let replay = 0; replay < 6; replay += 1) {
repeatedCtoMerge = __chatStoreTestUtils.mergeMessagesIntoExisting(
repeatedCtoMerge,
[{ ...ctoCompanyFinal, metadata: { ...ctoCompanyFinal.metadata } }],
)
assert.equal(repeatedCtoMerge.length, 1)
assert.equal(
repeatedCtoMerge[0].content,
ctoDispatchContent,
`identical MERGE replay ${replay + 1} must preserve the complete source text`,
)
}
const truncatedCtoContent = ctoDispatchContent.slice(
ctoDispatchContent.indexOf('OpenOPC Source Code Architecture Deep-Dive Analysis**'),
)
const truncatedCtoFinal: ChatMessage = {
...ctoCompanyFinal,
content: truncatedCtoContent,
}
let interleavedCtoReplay = [ctoCompanyFinal]
for (const replayedMessage of [
truncatedCtoFinal,
ctoCompanyFinal,
truncatedCtoFinal,
ctoCompanyFinal,
]) {
interleavedCtoReplay = __chatStoreTestUtils.mergeMessagesIntoExisting(
interleavedCtoReplay,
[{ ...replayedMessage, metadata: { ...replayedMessage.metadata } }],
)
assert.equal(interleavedCtoReplay.length, 1)
assert.equal(
interleavedCtoReplay[0].content,
ctoDispatchContent,
'a same-identity truncated cache replay must neither replace nor duplicate the complete message',
)
}
const repairedCtoReplay = __chatStoreTestUtils.mergeMessagesIntoExisting(
[truncatedCtoFinal],
[ctoCompanyFinal],
)
assert.equal(repairedCtoReplay.length, 1)
assert.equal(repairedCtoReplay[0].content, ctoDispatchContent)
assert.equal(
__chatStoreTestUtils.mergeMessagesIntoExisting(repairedCtoReplay, [truncatedCtoFinal])[0].content,
ctoDispatchContent,
'once a complete same-identity source arrives, later truncated replays must not regress it',
)
const mountedHighPriorityResult: ChatMessage = {
...companyRoleResult,
id: 'mounted-high-result',
@@ -23,8 +23,8 @@ function messageMetadata(message: ChatMessage): Record<string, unknown> {
return (message.metadata ?? {}) as Record<string, unknown>
}
function normalizeMessageContent(content: string): string {
const normalized = String(content ?? '')
function normalizeMessageFormatting(content: string): string {
return String(content ?? '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.split('\n')
@@ -32,6 +32,10 @@ function normalizeMessageContent(content: string): string {
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
function normalizeMessageContent(content: string): string {
const normalized = normalizeMessageFormatting(content)
const titleStripped = stripNarrativeTitlePrefix(normalized)
const paragraphs = titleStripped.split(/\n{2,}/).map(part => part.trim()).filter(Boolean)
if (paragraphs.length > 1 && /^Verification:\s/i.test(paragraphs[paragraphs.length - 1])) {
@@ -41,21 +45,40 @@ function normalizeMessageContent(content: string): string {
}
function stripNarrativeTitlePrefix(content: string): string {
const trimmed = String(content || '').trim()
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
if (markdownTitle) {
let trimmed = String(content || '').trim()
// Only an explicit, first-line "**Title**: body" wrapper is removable.
// Searching for an arbitrary `: ` in the first N characters is destructive:
// ordinary Markdown such as "**Work Item 1: ...**" and list fields such as
// "- ID: ..." would be peeled one layer at a time on repeated syncs.
for (;;) {
const markdownTitle = trimmed.match(/^\*\*([^\r\n]{8,160}?)\*\*:(?:[ \t]+|\r?\n+)([\s\S]+)$/)
if (!markdownTitle) return trimmed
const body = markdownTitle[2].trim()
if (body.length >= 80) return body
if (body.length < 80 || body === trimmed) return trimmed
trimmed = body
}
const colonIndex = trimmed.indexOf(': ')
if (colonIndex < 8 || colonIndex > 160) return trimmed
}
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
const body = trimmed.slice(colonIndex + 2).trim()
if (body.length < 80) return trimmed
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
if (/^(https?|file)$/i.test(prefix)) return trimmed
return body
function losslessIdentityContent(
existing: ChatMessage,
candidate: ChatMessage,
preferred: ChatMessage,
sharesConcreteIdentity: boolean,
): string {
if (!sharesConcreteIdentity) return preferred.content
const existingComparable = normalizeMessageFormatting(existing.content)
const candidateComparable = normalizeMessageFormatting(candidate.content)
if (!existingComparable || !candidateComparable || existingComparable === candidateComparable) {
return preferred.content
}
// A cache/detail replay can contain a prefix-truncated copy of the same
// persistent message. Preserve the lossless source regardless of arrival
// order; unrelated edits still follow the normal preference rules.
if (existingComparable.endsWith(candidateComparable)) return existing.content
if (candidateComparable.endsWith(existingComparable)) return candidate.content
return preferred.content
}
function messageIdentityKeys(message: ChatMessage): Set<string> {
@@ -66,6 +89,9 @@ function messageIdentityKeys(message: ChatMessage): Set<string> {
for (const value of [
message.id,
typeof metadata.ui_message_id === 'string' ? metadata.ui_message_id : '',
typeof metadata.result_delivery_id === 'string' && metadata.result_delivery_id.trim()
? `delivery:${metadata.result_delivery_id.trim()}`
: '',
checkpointType && checkpointId ? `checkpoint:${checkpointType}:${checkpointId}` : '',
]) {
const normalized = String(value ?? '').trim()
@@ -81,7 +107,7 @@ function scopedMessageIdentityKeys(message: ChatMessage): Set<string> {
}
function isDerivedIdentityKey(value: string): boolean {
return value.startsWith('checkpoint:')
return value.startsWith('checkpoint:') || value.startsWith('delivery:')
}
function messageTimestamp(message: ChatMessage): number {
@@ -229,18 +255,17 @@ function mergeDuplicateMessages(
const existingIds = messageIdentityKeys(existing)
const candidateIds = messageIdentityKeys(candidate)
let sharesConcreteIdentity = false
let canonicalId = ''
for (const id of existingIds) {
if (candidateIds.has(id) && !isDerivedIdentityKey(id)) {
canonicalId = id
break
if (!candidateIds.has(id)) continue
if (!isDerivedIdentityKey(id)) {
sharesConcreteIdentity = true
if (!canonicalId) canonicalId = id
}
}
const normalizedContent = normalizeMessageContent(preferred.content)
const content = normalizedContent && normalizedContent === normalizeMessageContent(secondary.content)
? normalizedContent
: preferred.content
const content = losslessIdentityContent(existing, candidate, preferred, sharesConcreteIdentity)
const existingCheckpointId = String(messageMetadata(existing).checkpoint_id ?? '').trim()
const candidateCheckpointId = String(messageMetadata(candidate).checkpoint_id ?? '').trim()
@@ -17,6 +17,16 @@ assert.match(
assert.match(messageListSource, /scrollPolicy = 'follow'/, 'main transcript behavior should default to follow mode')
assert.doesNotMatch(messageListSource, /useVirtualizer/, 'chat transcript must use stable normal DOM rows')
assert.doesNotMatch(messageListSource, /PROGRAMMATIC_SCROLL_GRACE_MS/, 'scroll behavior must not regress to timer-based intent guessing')
assert.doesNotMatch(
messageListSource,
/seenNarrativeMessages|seenProjectUpdates/,
'MessageList must not independently remove durable rows using rendered content',
)
assert.match(
messageListSource,
/terminalAssistantTurnId\(message\)/,
'draft suppression must use the shared committed-turn resolver',
)
const progressWithoutServerId = {
type: 'status_change' as const,
@@ -147,7 +157,16 @@ const companyTurnKeys = [
metadata: { kind: 'company_role_result', canonical_turn_id: sharedCompanyTurn },
}),
]
assert.equal(new Set(companyTurnKeys).size, companyTurnKeys.length, 'distinct company rows sharing a turn need unique DOM keys')
assert.equal(
companyTurnKeys[1],
companyTurnKeys[2],
'company draft/final surfaces for one canonical turn must reuse the same DOM slot',
)
assert.notEqual(
companyTurnKeys[2],
companyTurnKeys[3],
'a separately committed role result keeps its result-delivery identity',
)
const narrativeItems = buildNarrativeMessageItems([
baseMessage('m1', '[Company:cto::execute::abc] starting Research source reliability', 1000),
@@ -163,35 +182,63 @@ assert.equal(narrativeItems[0].kind === 'ops-bundle' ? narrativeItems[0].events.
assert.equal(narrativeItems[1].kind, 'message')
assert.equal(narrativeItems[2].kind, 'ops-bundle')
const longResult = 'Completed the focused recheck and produced the QA artifact with caveats for downstream aggregation.'
const dedupedProjectUpdates = buildNarrativeMessageItems([
baseMessage('u1', prefixedPayload, 2000, 'qa_analyst'),
baseMessage('u2', `**Report #1: Recheck remediated screen**: ${prefixedPayload}`, 2000, 'qa_analyst'),
], { isCompanyRuntime: true, detailMode: 'summary' })
assert.equal(dedupedProjectUpdates.length, 1)
assert.equal(
dedupedProjectUpdates.length,
2,
'renderer must preserve distinct project-update rows; upstream identity owns consolidation',
)
assert.equal(dedupedProjectUpdates[0].kind, 'message')
assert.equal(dedupedProjectUpdates[0].kind === 'message' ? dedupedProjectUpdates[0].msg.id : '', 'u1')
const longResult = 'Completed the focused recheck and produced the QA artifact with caveats for downstream aggregation.'
const dedupedNarrativeMessages = buildNarrativeMessageItems([
const identicalDurableNarratives = buildNarrativeMessageItems([
baseMessage('same-content-1', longResult, 2500, 'qa_analyst'),
baseMessage('same-content-2', longResult, 2500, 'qa_analyst'),
], { isCompanyRuntime: true, detailMode: 'summary' })
assert.deepEqual(
identicalDurableNarratives.map(item => item.kind === 'message' ? item.msg.id : item.id),
['same-content-1', 'same-content-2'],
'distinct stable identities must survive even when sender, timestamp, and content are identical',
)
const ambiguousNarrativeMessages = buildNarrativeMessageItems([
baseMessage('n1', longResult, 3000, 'qa_analyst'),
baseMessage('n2', `Recheck remediated ten-bagger candidate screen: ${longResult}`, 3000, 'qa_analyst'),
], { isCompanyRuntime: true, detailMode: 'summary' })
assert.equal(dedupedNarrativeMessages.length, 1)
assert.equal(dedupedNarrativeMessages[0].kind === 'message' ? dedupedNarrativeMessages[0].msg.id : '', 'n1')
assert.equal(
ambiguousNarrativeMessages.length,
2,
'plain narrative prefixes are content and must not be stripped to guess message identity',
)
const duplicatedResultSurface = buildNarrativeMessageItems([
{
...baseMessage('r1', longResult, 4000, 'chao'),
metadata: { source: 'engine', transcript_kind: 'child_task_result' },
metadata: {
source: 'engine',
transcript_kind: 'child_task_result',
result_delivery_id: 'delivery-r1',
},
},
{
...baseMessage('r2', `Deliver final result to user: ${longResult}`, 4500, 'system'),
senderName: 'Company Member',
metadata: { source: 'runtime_event', kind: 'worker_notification', notification_kind: 'task_complete' },
metadata: {
source: 'engine',
transcript_kind: 'child_result',
result_delivery_id: 'delivery-r1',
},
},
], { isCompanyRuntime: true, detailMode: 'summary' })
assert.equal(duplicatedResultSurface.length, 1)
assert.equal(duplicatedResultSurface[0].kind === 'message' ? duplicatedResultSurface[0].msg.id : '', 'r1')
assert.equal(
duplicatedResultSurface.length,
2,
'MessageList must not run a second result consolidator after the store/company projection',
)
const fullItems = buildNarrativeMessageItems([
baseMessage('m1', '[Company:cto::execute::abc] starting Research source reliability', 1000),
@@ -4,6 +4,7 @@ import type { ProgressEntry, RoleWorkItemSummary, Session, WorkItemProgressEntry
import { progressEntryKey } from '../lib/progressEntryKey'
import { stableMessageTimelineKey } from '../lib/messageTimelineIdentity'
import { isMessageVisibleAtDetailLevel, resultSurfaceDedupeKey } from '../lib/workItemSessions'
import { resolveCanonicalTurnId, terminalAssistantTurnId } from '../lib/turnIdentity'
import { IconCopy, IconCheck, IconChat, IconSparkle, IconShield, IconActivity, IconChevron } from './SvgIcons'
import { AgentProgressBlock, AgentProgressEntryCard, INLINE_PROGRESS_ENTRY_TYPES } from './AgentProgressBlock'
import { MarkdownBody } from './MarkdownBody'
@@ -345,13 +346,6 @@ export function messageTimelineKey(message: ChatMessage): string {
return stableMessageTimelineKey(message)
}
function terminalAssistantTurnId(message: ChatMessage): string {
const timelineKey = messageTimelineKey(message)
return timelineKey.startsWith('turn:assistant:')
? timelineKey.slice('turn:assistant:'.length)
: ''
}
function formatTime(ts: number) {
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })
}
@@ -400,44 +394,6 @@ function compactWhitespace(value: string): string {
return value.replace(/\s+/g, ' ').trim()
}
function stripNarrativeTitlePrefix(content: string): string {
const trimmed = String(content || '').trim()
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
if (markdownTitle) {
const body = markdownTitle[2].trim()
if (body.length >= 80) return body
}
const colonIndex = trimmed.indexOf(': ')
if (colonIndex < 8 || colonIndex > 160) return trimmed
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
const body = trimmed.slice(colonIndex + 2).trim()
if (body.length < 80) return trimmed
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
if (/^(https?|file)$/i.test(prefix)) return trimmed
return body
}
function isResultSurfaceMessage(message: ChatMessage): boolean {
const transcriptKind = String(message.metadata?.transcript_kind ?? message.metadata?.kind ?? '').trim()
if ([
'runtime_v2_assistant',
'runtime_v2_company_assistant',
'top_level_reply',
'company_role_result',
'company_role_result_retry',
'child_task_result',
'child_task_result_retry',
'child_result',
].includes(transcriptKind)) {
return true
}
if (String(message.metadata?.kind ?? '').trim() === 'worker_notification') {
return true
}
return false
}
function parseJsonObjectText(content: string): Record<string, unknown> | null {
const trimmed = String(content || '').trim()
if (!trimmed) return null
@@ -830,8 +786,6 @@ export function buildNarrativeMessageItems(
const items: TimelineItem[] = []
let bundle: SystemOpsBundleEvent[] = []
let bundleSortOrder = 0
const seenProjectUpdates = new Set<string>()
const seenNarrativeMessages = new Set<string>()
const flushBundle = () => {
if (bundle.length === 0) return
@@ -854,33 +808,6 @@ export function buildNarrativeMessageItems(
bundle.push(ops)
return
}
const projectUpdate = detailMode === 'summary' ? parseProjectUpdatePayload(msg.content) : null
if (projectUpdate) {
const dedupeKey = [
msg.sender,
Math.round(msg.timestamp / 1000),
projectUpdate.kind,
compactWhitespace(projectUpdate.summary || projectUpdate.acceptanceSummary || '').slice(0, 500),
projectUpdate.verdict ?? '',
projectUpdate.deliverables.map(item => `${item.name}:${item.path}`).join('|').slice(0, 800),
].join('\u0001')
if (seenProjectUpdates.has(dedupeKey)) return
seenProjectUpdates.add(dedupeKey)
}
if (detailMode === 'summary' && !isCheckpointCardMetadata(msg.metadata)) {
const canonicalContent = compactWhitespace(stripNarrativeTitlePrefix(msg.content)).slice(0, 1200)
if (canonicalContent) {
const dedupeKey = isResultSurfaceMessage(msg)
? ['result', canonicalContent].join('\u0001')
: [
msg.sender,
Math.round(msg.timestamp / 1000),
canonicalContent,
].join('\u0001')
if (seenNarrativeMessages.has(dedupeKey)) return
seenNarrativeMessages.add(dedupeKey)
}
}
flushBundle()
items.push({
kind: 'message',
@@ -1331,7 +1258,7 @@ export const MessageList = React.memo(function MessageList({
for (const message of timelineMessages) {
const thinking = String(message.metadata?.runtime_thinking ?? '').trim()
if (!thinking) continue
const turnId = String(message.metadata?.canonical_turn_id ?? message.metadata?.turn_id ?? '').trim()
const turnId = resolveCanonicalTurnId(message.metadata)
if (turnId && thinkingProgressTurnIds.has(turnId)) continue
entries.push({
type: 'thinking' as const,