fix(ui): stabilize company chat result topology
This commit is contained in:
@@ -1,4 +1,103 @@
|
||||
import type { ChatMessage } from '../types/chat'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './turnIdentity'
|
||||
|
||||
const COMMITTED_RESULT_SURFACE_KINDS = new Set([
|
||||
'child_task_result',
|
||||
'child_task_result_retry',
|
||||
'company_role_result',
|
||||
'company_role_result_retry',
|
||||
'child_result',
|
||||
'top_level_reply',
|
||||
])
|
||||
|
||||
const RUNTIME_RESULT_SURFACE_KINDS = new Set([
|
||||
'runtime_v2_assistant',
|
||||
'runtime_v2_company_assistant',
|
||||
])
|
||||
|
||||
function metadataValue(metadata: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = String(metadata[key] ?? '').trim()
|
||||
if (value) return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function resultGenerationSuffix(metadata: Record<string, unknown>, kind: string): string {
|
||||
const explicitAttempt = metadataValue(
|
||||
metadata,
|
||||
'result_attempt',
|
||||
'attempt',
|
||||
'attempt_index',
|
||||
'retry_count',
|
||||
'retryCount',
|
||||
)
|
||||
const attempt = explicitAttempt || (kind.endsWith('_retry') ? 'retry' : '')
|
||||
const revision = metadataValue(
|
||||
metadata,
|
||||
'result_revision',
|
||||
'delivery_revision',
|
||||
'revision',
|
||||
)
|
||||
return [
|
||||
attempt ? `attempt:${attempt}` : '',
|
||||
revision ? `revision:${revision}` : '',
|
||||
].filter(Boolean).join(':')
|
||||
}
|
||||
|
||||
function withResultGeneration(
|
||||
base: string,
|
||||
metadata: Record<string, unknown>,
|
||||
kind: string,
|
||||
): string {
|
||||
const suffix = resultGenerationSuffix(metadata, kind)
|
||||
return suffix ? `${base}:${suffix}` : base
|
||||
}
|
||||
|
||||
/** Stable protocol identity shared by mirrors of one committed result. */
|
||||
export function stableResultDeliveryKey(message: ChatMessage): string {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const kind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (!COMMITTED_RESULT_SURFACE_KINDS.has(kind) && !RUNTIME_RESULT_SURFACE_KINDS.has(kind)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const deliveryId = metadataValue(
|
||||
metadata,
|
||||
'canonical_delivery_id',
|
||||
'result_delivery_id',
|
||||
'delivery_id',
|
||||
)
|
||||
if (deliveryId) return `delivery:${deliveryId}`
|
||||
|
||||
if (RUNTIME_RESULT_SURFACE_KINDS.has(kind)) {
|
||||
const turnId = resolveCanonicalTurnId(metadata)
|
||||
return turnId ? `turn:${turnId}` : ''
|
||||
}
|
||||
|
||||
// A committed result belongs to a task/work-item delivery, not merely to
|
||||
// the surrounding conversation turn. Parallel roles commonly share that
|
||||
// turn, so a turn-only fallback would collapse independent deliveries.
|
||||
const explicitSourceTaskId = metadataValue(metadata, 'source_task_id', 'child_task_id')
|
||||
const sourceTaskId = explicitSourceTaskId || (
|
||||
kind === 'top_level_reply'
|
||||
? ''
|
||||
: metadataValue(metadata, 'task_id', 'taskId')
|
||||
)
|
||||
if (sourceTaskId) {
|
||||
return withResultGeneration(`source-task:${sourceTaskId}`, metadata, kind)
|
||||
}
|
||||
|
||||
const workItemId = metadataValue(metadata, 'work_item_id', 'work_item_projection_id')
|
||||
if (workItemId) {
|
||||
return withResultGeneration(`work-item:${workItemId}`, metadata, kind)
|
||||
}
|
||||
|
||||
const childSessionId = metadataValue(metadata, 'child_session_id')
|
||||
return childSessionId
|
||||
? withResultGeneration(`child-session:${childSessionId}`, metadata, kind)
|
||||
: ''
|
||||
}
|
||||
|
||||
export function stableMessageTimelineKey(message: ChatMessage): string {
|
||||
const metadata = message.metadata ?? {}
|
||||
@@ -21,10 +120,19 @@ export function stableMessageTimelineKey(message: ChatMessage): string {
|
||||
const retainedTimelineId = String(metadata.ui_timeline_id ?? '').trim()
|
||||
if (retainedTimelineId) return retainedTimelineId
|
||||
|
||||
const turnId = String(metadata.canonical_turn_id ?? metadata.turn_id ?? '').trim()
|
||||
if (!isUserTurn && turnId && transcriptKind === 'runtime_v2_assistant') {
|
||||
// Only a streamed runtime terminal owns the live draft's React slot.
|
||||
// Committed result surfaces may expose the same conversation turn through
|
||||
// terminalAssistantTurnId, but their row identity must remain delivery/task
|
||||
// scoped so parallel role results cannot collide.
|
||||
const turnId = RUNTIME_RESULT_SURFACE_KINDS.has(transcriptKind)
|
||||
? terminalAssistantTurnId(message)
|
||||
: ''
|
||||
if (!isUserTurn && turnId) {
|
||||
return `turn:assistant:${turnId}`
|
||||
}
|
||||
|
||||
const resultDeliveryKey = stableResultDeliveryKey(message)
|
||||
if (!isUserTurn && resultDeliveryKey) return `result:${resultDeliveryKey}`
|
||||
|
||||
return `message:${message.id}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ChatMessage, ChatMessageMeta } from '../types/chat'
|
||||
|
||||
const TERMINAL_ASSISTANT_KINDS = new Set([
|
||||
'runtime_v2_assistant',
|
||||
'runtime_v2_company_assistant',
|
||||
'child_task_result',
|
||||
'child_task_result_retry',
|
||||
'company_role_result',
|
||||
'company_role_result_retry',
|
||||
'child_result',
|
||||
'top_level_reply',
|
||||
])
|
||||
|
||||
export function resolveCanonicalTurnId(
|
||||
metadata: ChatMessageMeta | Record<string, unknown> | null | undefined,
|
||||
): string {
|
||||
const source = (metadata ?? {}) as Record<string, unknown>
|
||||
for (const key of [
|
||||
'canonical_turn_id',
|
||||
'conversation_turn_id',
|
||||
'turn_id',
|
||||
'execution_turn_id',
|
||||
]) {
|
||||
const value = String(source[key] ?? '').trim()
|
||||
if (value) return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function terminalAssistantTurnId(message: ChatMessage): string {
|
||||
if (message.sender === 'user') return ''
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const kind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (!TERMINAL_ASSISTANT_KINDS.has(kind)) return ''
|
||||
if (kind === 'runtime_v2_company_assistant') {
|
||||
// Company tool-call iterations intentionally share this transcript kind
|
||||
// and canonical turn with the final answer. Treating every iteration as
|
||||
// terminal hides the live draft during detail refresh, then grows it back
|
||||
// on the next delta. Only the actual final surface may replace the draft.
|
||||
const uiMessageId = String(metadata.ui_message_id ?? '').trim()
|
||||
const isFinal = metadata.company_final_turn === true
|
||||
|| !!String(metadata.result_delivery_id ?? '').trim()
|
||||
|| uiMessageId.startsWith('runtime-v2-company-assistant-final:')
|
||||
// Snapshot translation retains final-vs-intermediate as visibility even
|
||||
// when reading records created before structured delivery ids existed.
|
||||
|| String(metadata.detail_visibility ?? '').trim() === 'summary'
|
||||
if (!isFinal) return ''
|
||||
}
|
||||
return resolveCanonicalTurnId(metadata)
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import assert from 'node:assert/strict'
|
||||
import type { ChatMessage } from '../types/chat'
|
||||
import type { Session } from '../types/kanban'
|
||||
import { mapBackendSession, mergeSessionDetailHasMore } from './collabSync'
|
||||
import { stableMessageTimelineKey, stableResultDeliveryKey } from './messageTimelineIdentity'
|
||||
import { canonicalizeSessionExecutionIdentity } from './sessionIdentity'
|
||||
import { deriveCompanyRuntimeDisplayStatus, getConversationHeaderSession, getConversationSessionView, getWorkItemChildSessions, getWorkItemRoleSessions, mergeConversationMessages, projectSessionConversation, selectCompanySummaryMessages } from './workItemSessions'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './turnIdentity'
|
||||
import { deriveCompanyRuntimeDisplayStatus, getConversationHeaderSession, getConversationSessionView, getWorkItemChildSessions, getWorkItemRoleSessions, mergeConversationMessages, projectSessionConversation, resultSurfaceDedupeKey, selectCompanySummaryMessages } from './workItemSessions'
|
||||
|
||||
function makeSession(overrides: Partial<Session> & Pick<Session, 'taskId' | 'channelId' | 'title' | 'status' | 'columnId' | 'assigneeIds' | 'priority' | 'tags' | 'progressLog' | 'createdAt' | 'updatedAt' | 'messageCount'>): Session {
|
||||
return {
|
||||
@@ -338,12 +340,380 @@ assert.deepEqual(
|
||||
[
|
||||
'parent-user',
|
||||
'canonical-role-result',
|
||||
'summary-company-final',
|
||||
'summary-terminal-b',
|
||||
'child-checkpoint',
|
||||
'child-checkpoint-response',
|
||||
].sort(),
|
||||
)
|
||||
|
||||
const companyRuntimeTurn = resultMessage(
|
||||
'company-runtime-turn',
|
||||
'session:company-child',
|
||||
'A company runtime terminal surface.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
canonical_turn_id: 'company-turn-0009',
|
||||
result_delivery_id: 'company-delivery-0009',
|
||||
},
|
||||
'assistant',
|
||||
)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey(companyRuntimeTurn),
|
||||
'turn:assistant:company-turn-0009',
|
||||
)
|
||||
|
||||
const conversationOnlyRuntimeTurn = resultMessage(
|
||||
'company-runtime-conversation-turn',
|
||||
'session:company-child',
|
||||
'A terminal surface with only its conversation identity.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
conversation_turn_id: 'conversation-turn-only',
|
||||
turn_id: 'iteration-turn-must-not-win',
|
||||
execution_turn_id: 'execution-turn-must-not-win',
|
||||
detail_visibility: 'summary',
|
||||
},
|
||||
'assistant',
|
||||
)
|
||||
assert.equal(
|
||||
resolveCanonicalTurnId(conversationOnlyRuntimeTurn.metadata),
|
||||
'conversation-turn-only',
|
||||
)
|
||||
assert.equal(terminalAssistantTurnId(conversationOnlyRuntimeTurn), 'conversation-turn-only')
|
||||
assert.equal(
|
||||
terminalAssistantTurnId({
|
||||
...conversationOnlyRuntimeTurn,
|
||||
metadata: {
|
||||
...conversationOnlyRuntimeTurn.metadata,
|
||||
detail_visibility: 'full',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
'a company tool-call iteration must not suppress the active draft',
|
||||
)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey(conversationOnlyRuntimeTurn),
|
||||
'turn:assistant:conversation-turn-only',
|
||||
)
|
||||
assert.equal(
|
||||
terminalAssistantTurnId({
|
||||
...conversationOnlyRuntimeTurn,
|
||||
metadata: {
|
||||
...conversationOnlyRuntimeTurn.metadata,
|
||||
transcript_kind: 'company_role_result',
|
||||
},
|
||||
}),
|
||||
'conversation-turn-only',
|
||||
'committed result kinds must participate in terminal-turn matching',
|
||||
)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey({
|
||||
...conversationOnlyRuntimeTurn,
|
||||
metadata: {
|
||||
...conversationOnlyRuntimeTurn.metadata,
|
||||
transcript_kind: 'company_role_result',
|
||||
source_task_id: 'committed-source-task',
|
||||
},
|
||||
}),
|
||||
'result:source-task:committed-source-task',
|
||||
'a committed terminal may expose its turn for matching without taking the runtime draft key',
|
||||
)
|
||||
|
||||
const project0009CtoResult = [
|
||||
'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, collaboration policy, seat executor pattern, and self-evolution mechanisms.',
|
||||
'',
|
||||
'**Work Item 2: External Multi-Agent Frameworks Architecture Research**',
|
||||
'- ID: `d0307208-6b95-44c1-9b51-6bf073bbdcef`',
|
||||
'- Owner: senior_engineer',
|
||||
'- Scope: `external-frameworks-research`',
|
||||
'- Output: `/data2/bjdwhzzh/project-hku/OpenOPC_workplace/0009/external-frameworks-analysis.md`',
|
||||
'',
|
||||
'Both are independent and can execute in parallel. The runtime will monitor their completion.',
|
||||
].join('\n')
|
||||
|
||||
const project0009WorkItemSuffix = project0009CtoResult.slice(
|
||||
project0009CtoResult.indexOf('OpenOPC Source Code Architecture Deep-Dive Analysis'),
|
||||
)
|
||||
const project0009IdSuffix = project0009CtoResult.slice(
|
||||
project0009CtoResult.indexOf('`1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82`'),
|
||||
)
|
||||
|
||||
// Content fallback must not treat arbitrary Markdown colons as removable
|
||||
// narrative wrappers. The old normalization repeatedly turned the full 0009
|
||||
// result into these two shorter variants, which changed the rendered height.
|
||||
const fallback0009Full = resultMessage(
|
||||
'0009-fallback-full',
|
||||
'session:company-root',
|
||||
project0009CtoResult,
|
||||
{ source: 'engine', transcript_kind: 'child_result' },
|
||||
'assistant',
|
||||
)
|
||||
const fallback0009WorkItem = resultMessage(
|
||||
'0009-fallback-work-item',
|
||||
'session:company-child',
|
||||
project0009WorkItemSuffix,
|
||||
{ source: 'engine', transcript_kind: 'runtime_v2_assistant' },
|
||||
'assistant',
|
||||
)
|
||||
const fallback0009Id = resultMessage(
|
||||
'0009-fallback-id',
|
||||
'session:company-child',
|
||||
project0009IdSuffix,
|
||||
{ source: 'engine', transcript_kind: 'runtime_v2_assistant' },
|
||||
'assistant',
|
||||
)
|
||||
assert.notEqual(resultSurfaceDedupeKey(fallback0009Full), resultSurfaceDedupeKey(fallback0009WorkItem))
|
||||
assert.notEqual(resultSurfaceDedupeKey(fallback0009WorkItem), resultSurfaceDedupeKey(fallback0009Id))
|
||||
|
||||
const committed0009Parent = {
|
||||
...resultMessage(
|
||||
'0009-parent-result',
|
||||
'session:company-root',
|
||||
project0009CtoResult,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_result',
|
||||
source_task_id: 'cto-task-0009',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_000,
|
||||
}
|
||||
const committed0009Child = {
|
||||
...resultMessage(
|
||||
'0009-child-result',
|
||||
'session:company-child',
|
||||
project0009CtoResult,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_task_result',
|
||||
task_id: 'cto-task-0009',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_010,
|
||||
}
|
||||
const raw0009WorkItem = {
|
||||
...fallback0009WorkItem,
|
||||
metadata: {
|
||||
...fallback0009WorkItem.metadata,
|
||||
detail_visibility: 'summary' as const,
|
||||
canonical_turn_id: 'cto-turn-0009',
|
||||
},
|
||||
timestamp: 2_020,
|
||||
}
|
||||
const raw0009Id = {
|
||||
...fallback0009Id,
|
||||
metadata: {
|
||||
...fallback0009Id.metadata,
|
||||
detail_visibility: 'summary' as const,
|
||||
canonical_turn_id: 'cto-turn-0009',
|
||||
},
|
||||
timestamp: 2_030,
|
||||
}
|
||||
|
||||
for (const messages of [
|
||||
[committed0009Parent, committed0009Child, raw0009WorkItem, raw0009Id],
|
||||
[raw0009Id, raw0009WorkItem, committed0009Child, committed0009Parent],
|
||||
]) {
|
||||
const summary = selectCompanySummaryMessages(messages, 'session:company-root')
|
||||
assert.equal(summary.length, 1)
|
||||
assert.equal(summary[0]?.id, '0009-child-result')
|
||||
assert.equal(summary[0]?.content, project0009CtoResult)
|
||||
assert.equal(stableMessageTimelineKey(summary[0]!), 'result:source-task:cto-task-0009')
|
||||
}
|
||||
|
||||
const sameRoleText = 'The role completed its independent architecture analysis and committed the result.'
|
||||
const multiRoleSummary = selectCompanySummaryMessages([
|
||||
resultMessage(
|
||||
'0009-cto-role-result',
|
||||
'session:company-cto',
|
||||
sameRoleText,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
work_item_projection_id: '0009-cto-work-item',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
resultMessage(
|
||||
'0009-coo-role-result',
|
||||
'session:company-coo',
|
||||
sameRoleText,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
work_item_projection_id: '0009-coo-work-item',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
], 'session:company-root')
|
||||
assert.deepEqual(
|
||||
multiRoleSummary.map(message => stableMessageTimelineKey(message)).sort(),
|
||||
[
|
||||
'result:work-item:0009-coo-work-item',
|
||||
'result:work-item:0009-cto-work-item',
|
||||
],
|
||||
)
|
||||
|
||||
const sharedConversationTurn = 'shared-company-conversation-turn'
|
||||
const parallelRoleResults = [
|
||||
resultMessage(
|
||||
'parallel-cto-result',
|
||||
'session:company-cto',
|
||||
'CTO completed the architecture assessment.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
canonical_turn_id: sharedConversationTurn,
|
||||
work_item_projection_id: 'architecture-assessment',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
resultMessage(
|
||||
'parallel-coo-result',
|
||||
'session:company-coo',
|
||||
'COO completed the feature assessment.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
canonical_turn_id: sharedConversationTurn,
|
||||
work_item_projection_id: 'feature-assessment',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
]
|
||||
assert.deepEqual(
|
||||
parallelRoleResults.map(resultSurfaceDedupeKey),
|
||||
[
|
||||
'result:work-item:architecture-assessment',
|
||||
'result:work-item:feature-assessment',
|
||||
],
|
||||
'parallel committed roles must use work-item identity before a shared conversation turn',
|
||||
)
|
||||
assert.equal(
|
||||
selectCompanySummaryMessages(parallelRoleResults, 'session:company-root').length,
|
||||
2,
|
||||
)
|
||||
|
||||
const versionedSourceResult = resultMessage(
|
||||
'versioned-source-result',
|
||||
'session:company-child',
|
||||
'Versioned result.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result_retry',
|
||||
source_task_id: 'source-task-versioned',
|
||||
retry_count: 2,
|
||||
delivery_revision: 4,
|
||||
} as ChatMessage['metadata'],
|
||||
'assistant',
|
||||
)
|
||||
assert.equal(
|
||||
stableResultDeliveryKey(versionedSourceResult),
|
||||
'source-task:source-task-versioned:attempt:2:revision:4',
|
||||
)
|
||||
|
||||
const fullEqualPriorityResult = {
|
||||
...resultMessage(
|
||||
'full-equal-priority',
|
||||
'session:company-child-a',
|
||||
'The complete authoritative body includes every required architectural conclusion and its supporting rationale.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'deterministic-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_100,
|
||||
}
|
||||
const truncatedEqualPriorityResult = {
|
||||
...resultMessage(
|
||||
'truncated-equal-priority',
|
||||
'session:company-child-b',
|
||||
'supporting rationale.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'deterministic-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_200,
|
||||
}
|
||||
for (const groups of [
|
||||
[[fullEqualPriorityResult], [truncatedEqualPriorityResult]],
|
||||
[[truncatedEqualPriorityResult], [fullEqualPriorityResult]],
|
||||
]) {
|
||||
const merged = mergeConversationMessages(groups)
|
||||
assert.equal(merged.length, 1)
|
||||
assert.equal(merged[0]?.id, 'full-equal-priority')
|
||||
assert.equal(merged[0]?.content, fullEqualPriorityResult.content)
|
||||
assert.equal(merged[0]?.timestamp, 2_100)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey(merged[0]!),
|
||||
'result:delivery:deterministic-delivery',
|
||||
)
|
||||
const replayed = mergeConversationMessages([
|
||||
merged,
|
||||
[truncatedEqualPriorityResult],
|
||||
])
|
||||
assert.equal(replayed.length, 1)
|
||||
assert.equal(replayed[0]?.id, 'full-equal-priority')
|
||||
assert.equal(replayed[0]?.content, fullEqualPriorityResult.content)
|
||||
}
|
||||
|
||||
const equalLengthStableWinner = {
|
||||
...resultMessage(
|
||||
'z-stable-winner',
|
||||
'session:company-child-a',
|
||||
'BBBB',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'equal-length-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 3_200,
|
||||
}
|
||||
const equalLengthLoser = {
|
||||
...resultMessage(
|
||||
'a-stable-loser',
|
||||
'session:company-child-b',
|
||||
'AAAA',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'equal-length-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 3_100,
|
||||
}
|
||||
for (const groups of [
|
||||
[[equalLengthStableWinner], [equalLengthLoser]],
|
||||
[[equalLengthLoser], [equalLengthStableWinner]],
|
||||
]) {
|
||||
const firstMerge = mergeConversationMessages(groups)
|
||||
assert.equal(firstMerge[0]?.id, 'z-stable-winner')
|
||||
assert.equal(firstMerge[0]?.content, 'BBBB')
|
||||
assert.equal(firstMerge[0]?.timestamp, 3_100)
|
||||
const replayed = mergeConversationMessages([firstMerge, [equalLengthLoser]])
|
||||
assert.equal(replayed[0]?.id, 'z-stable-winner')
|
||||
assert.equal(replayed[0]?.content, 'BBBB')
|
||||
}
|
||||
assert.equal(companyHeaderView?.status, 'running')
|
||||
assert.equal(companyHeaderView?.contextTokens, 0)
|
||||
assert.equal(companyHeaderView?.contextWindow, 128000)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ChatMessage } from '../types/chat'
|
||||
import type { ProgressEntry, Session } from '../types/kanban'
|
||||
import { getContextUsageMetrics } from './contextUsage'
|
||||
import { isSessionWorking } from './sessionRuntime'
|
||||
import { stableMessageTimelineKey } from './messageTimelineIdentity'
|
||||
import { stableMessageTimelineKey, stableResultDeliveryKey } from './messageTimelineIdentity'
|
||||
|
||||
const CONTEXT_TOKENS_RE = /(\d[\d,]*)\s*\/\s*(\d[\d,]*)\s+tokens/i
|
||||
const USED_PCT_RE = /(\d{1,3})%\s*used/i
|
||||
@@ -20,22 +20,32 @@ 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
|
||||
function normalizeResultContentFallback(content: string): string {
|
||||
let normalized = String(content || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
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
|
||||
// Some result mirrors add one explicit Markdown narrative label. Only
|
||||
// remove that anchored wrapper; a colon later in Markdown (for example
|
||||
// "**Work Item 1: ..." or "- ID: ...") is message content, not a title.
|
||||
for (;;) {
|
||||
const markdownTitle = normalized.match(/^\*\*([^\r\n]{8,160}?)\*\*:(?:[ \t]+|\r?\n+)([\s\S]+)$/)
|
||||
if (!markdownTitle) break
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length < 80 || body === normalized) break
|
||||
normalized = body
|
||||
}
|
||||
|
||||
const paragraphs = normalized.split(/\n{2,}/).map(part => part.trim()).filter(Boolean)
|
||||
if (paragraphs.length > 1 && /^Verification:\s/i.test(paragraphs[paragraphs.length - 1])) {
|
||||
normalized = paragraphs.slice(0, -1).join('\n\n').trim()
|
||||
}
|
||||
return compactWhitespace(normalized).slice(0, 2000)
|
||||
}
|
||||
|
||||
function resultSurfacePriority(message: ChatMessage): number {
|
||||
@@ -67,9 +77,52 @@ function resultSurfacePriority(message: ChatMessage): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
function normalizedResultContentLength(message: ChatMessage): number {
|
||||
return String(message.content ?? '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n')
|
||||
.trim()
|
||||
.length
|
||||
}
|
||||
|
||||
function resultAuthorityScore(message: ChatMessage): number {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
let score = 0
|
||||
if (String(metadata.source ?? '').trim() === 'engine') score += 4
|
||||
if (String(metadata.source_result_message_id ?? '').trim()) score += 2
|
||||
if (metadata.authoritative_output === true || metadata.canonical_result === true) score += 1
|
||||
return score
|
||||
}
|
||||
|
||||
function compareResultSurfacePreference(left: ChatMessage, right: ChatMessage): number {
|
||||
const numericComparisons: Array<[number, number]> = [
|
||||
[resultSurfacePriority(left), resultSurfacePriority(right)],
|
||||
[resultAuthorityScore(left), resultAuthorityScore(right)],
|
||||
[normalizedResultContentLength(left), normalizedResultContentLength(right)],
|
||||
]
|
||||
for (const [leftValue, rightValue] of numericComparisons) {
|
||||
if (leftValue !== rightValue) return leftValue > rightValue ? 1 : -1
|
||||
}
|
||||
const idComparison = left.id.localeCompare(right.id)
|
||||
if (idComparison !== 0) return idComparison
|
||||
const contentComparison = left.content.localeCompare(right.content)
|
||||
if (contentComparison !== 0) return contentComparison
|
||||
// Result chronology is later rewritten to the earliest underlying delivery.
|
||||
// Timestamp is therefore safe only after immutable identity/content have
|
||||
// tied; it must never be able to flip the selected surface on replay.
|
||||
if (left.timestamp === right.timestamp) return 0
|
||||
return left.timestamp > right.timestamp ? 1 : -1
|
||||
}
|
||||
|
||||
export function resultSurfaceDedupeKey(message: ChatMessage): string {
|
||||
if (resultSurfacePriority(message) <= 0) return ''
|
||||
const content = compactWhitespace(stripNarrativeTitlePrefix(message.content)).slice(0, 2000)
|
||||
const deliveryKey = stableResultDeliveryKey(message)
|
||||
if (deliveryKey) return `result:${deliveryKey}`
|
||||
|
||||
const content = normalizeResultContentFallback(message.content)
|
||||
return content ? `result:${content}` : ''
|
||||
}
|
||||
|
||||
@@ -492,7 +545,7 @@ export function mergeConversationMessages(messageGroups: ChatMessage[][]): ChatM
|
||||
const existingIndex = resultKeyIndex.get(resultKey)
|
||||
if (existingIndex !== undefined) {
|
||||
const existing = merged[existingIndex]
|
||||
const candidateWins = resultSurfacePriority(message) > resultSurfacePriority(existing)
|
||||
const candidateWins = compareResultSurfacePreference(message, existing) > 0
|
||||
const preferred = candidateWins ? message : existing
|
||||
const secondary = candidateWins ? existing : message
|
||||
merged[existingIndex] = {
|
||||
@@ -505,7 +558,7 @@ export function mergeConversationMessages(messageGroups: ChatMessage[][]): ChatM
|
||||
metadata: {
|
||||
...(secondary.metadata ?? {}),
|
||||
...(preferred.metadata ?? {}),
|
||||
ui_timeline_id: stableMessageTimelineKey(existing),
|
||||
ui_timeline_id: resultKey || stableMessageTimelineKey(existing),
|
||||
},
|
||||
}
|
||||
continue
|
||||
@@ -541,22 +594,6 @@ export function selectCompanySummaryMessages(
|
||||
messages: ChatMessage[],
|
||||
parentChannelId: string,
|
||||
): ChatMessage[] {
|
||||
const terminalAssistantTurn = (message: ChatMessage): string => {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const kind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (kind !== 'runtime_v2_assistant' && kind !== 'runtime_v2_company_assistant') return ''
|
||||
return String(metadata.canonical_turn_id ?? metadata.turn_id ?? '').trim()
|
||||
}
|
||||
const parentTerminalTurns = new Set(
|
||||
messages
|
||||
.filter(message => (
|
||||
message.channelId === parentChannelId
|
||||
&& isMessageVisibleAtDetailLevel(message, 'summary')
|
||||
))
|
||||
.map(terminalAssistantTurn)
|
||||
.filter(Boolean),
|
||||
)
|
||||
const childTerminalByTurn = new Map<string, ChatMessage>()
|
||||
const durableMessages: ChatMessage[] = []
|
||||
for (const message of messages) {
|
||||
if (message.channelId === parentChannelId) {
|
||||
@@ -584,38 +621,8 @@ export function selectCompanySummaryMessages(
|
||||
'top_level_reply',
|
||||
].includes(kind)) {
|
||||
durableMessages.push(message)
|
||||
continue
|
||||
}
|
||||
// Snapshot builder deliberately marks the final runtime surface as
|
||||
// summary-visible. Preserve that durable contract instead of reusing the
|
||||
// result-dedupe priority table as a visibility threshold.
|
||||
const isSummaryTerminal = String(metadata.detail_visibility ?? '').trim() === 'summary'
|
||||
&& (kind === 'runtime_v2_assistant' || kind === 'runtime_v2_company_assistant')
|
||||
if (!isSummaryTerminal) continue
|
||||
const turnId = terminalAssistantTurn(message)
|
||||
if (!turnId) {
|
||||
durableMessages.push(message)
|
||||
continue
|
||||
}
|
||||
if (parentTerminalTurns.has(turnId)) continue
|
||||
const existing = childTerminalByTurn.get(turnId)
|
||||
if (!existing) {
|
||||
childTerminalByTurn.set(turnId, message)
|
||||
continue
|
||||
}
|
||||
const existingPriority = resultSurfacePriority(existing)
|
||||
const candidatePriority = resultSurfacePriority(message)
|
||||
if (candidatePriority > existingPriority) {
|
||||
childTerminalByTurn.set(turnId, message)
|
||||
} else if (
|
||||
candidatePriority === existingPriority
|
||||
&& (message.timestamp > existing.timestamp
|
||||
|| (message.timestamp === existing.timestamp && message.id.localeCompare(existing.id) > 0))
|
||||
) {
|
||||
childTerminalByTurn.set(turnId, message)
|
||||
}
|
||||
}
|
||||
durableMessages.push(...childTerminalByTurn.values())
|
||||
return mergeConversationMessages([durableMessages])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user