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
+171 -84
View File
@@ -575,23 +575,43 @@ class ChatStore:
@staticmethod
def _strip_narrative_title_prefix(content: str) -> str:
trimmed = str(content or "").strip()
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
if markdown_title:
# Only discard an explicit, anchored narrative wrapper. The former
# fallback used the first ``": "`` anywhere in the first 160
# characters, so ordinary Markdown such as ``**Work Item 1: ...**``
# was progressively truncated every time a sync was merged. Peeling
# explicit nested wrappers to a fixed point keeps this comparison
# helper idempotent without interpreting body punctuation as structure.
while True:
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
if not markdown_title:
return trimmed
body = markdown_title.group(2).strip()
if len(body) >= 80:
return body
colon_index = trimmed.find(": ")
if colon_index < 8 or colon_index > 160:
return trimmed
prefix = trimmed[:colon_index].replace("*", "").strip()
body = trimmed[colon_index + 2 :].strip()
if len(body) < 80:
return trimmed
if not re.search(r"[A-Za-z\u4e00-\u9fff]", prefix):
return trimmed
if re.match(r"^(https?|file)$", prefix, flags=re.IGNORECASE):
return trimmed
return body
if len(body) < 80 or body == trimmed:
return trimmed
trimmed = body
@classmethod
def _select_duplicate_display_content(
cls,
preferred: dict[str, Any],
secondary: dict[str, Any],
) -> str:
"""Choose an exact input body; never persist the comparison key itself."""
preferred_content = str(preferred.get("content", "") or "")
secondary_content = str(secondary.get("content", "") or "")
preferred_key = cls._normalize_duplicate_content(preferred_content)
if not preferred_key or preferred_key != cls._normalize_duplicate_content(secondary_content):
return preferred_content
# If one real surface is already the comparison body while the other
# carries an explicit narrative wrapper/footer, reuse that real body.
# This preserves the historical visible answer without manufacturing
# display text from a lossy canonicalization function.
if (
secondary_content.strip() == preferred_key
and preferred_content.strip() != preferred_key
):
return secondary_content
return preferred_content
@staticmethod
def _message_timestamp(message: dict[str, Any]) -> float:
@@ -663,6 +683,9 @@ class ChatStore:
normalized = str(value or "").strip()
if normalized:
keys.add(normalized)
result_delivery_id = str(metadata.get("result_delivery_id", "") or "").strip()
if result_delivery_id:
keys.add(f"result_delivery:{result_delivery_id}")
return keys
@classmethod
@@ -681,10 +704,15 @@ class ChatStore:
cls,
existing: dict[str, Any],
candidate: dict[str, Any],
*,
prefer_candidate: bool = False,
) -> dict[str, Any]:
preferred = existing
secondary = candidate
if cls._message_preference_score(candidate) > cls._message_preference_score(existing):
if (
prefer_candidate
or cls._message_preference_score(candidate) > cls._message_preference_score(existing)
):
preferred = candidate
secondary = existing
@@ -694,12 +722,7 @@ class ChatStore:
secondary_meta = dict(secondary.get("metadata", {}) or {})
preferred_meta = dict(preferred.get("metadata", {}) or {})
merged["metadata"] = {**secondary_meta, **preferred_meta}
normalized_content = cls._normalize_duplicate_content(preferred.get("content", ""))
if (
normalized_content
and normalized_content == cls._normalize_duplicate_content(secondary.get("content", ""))
):
merged["content"] = normalized_content
merged["content"] = cls._select_duplicate_display_content(preferred, secondary)
shared_ids = cls._message_identity_keys(existing) & cls._message_identity_keys(candidate)
canonical_id = ""
@@ -742,7 +765,10 @@ class ChatStore:
return (
str(existing.get("sender", "") or "") == str(candidate.get("sender", "") or "")
and str(existing.get("sender_name", "") or "") == str(candidate.get("sender_name", "") or "")
and cls._normalize_duplicate_content(existing.get("content", "")) == cls._normalize_duplicate_content(candidate.get("content", ""))
# Display content is persisted data, not a duplicate-comparison
# key. Exact comparison lets authoritative transcript backfill
# repair a legacy row whose body was destructively normalized.
and str(existing.get("content", "") or "") == str(candidate.get("content", "") or "")
and cls._message_timestamp(existing) == cls._message_timestamp(candidate)
and str(existing.get("reply_to_id", "") or "") == str(candidate.get("reply_to_id", "") or "")
and list(existing.get("mentions", []) or []) == list(candidate.get("mentions", []) or [])
@@ -791,6 +817,55 @@ class ChatStore:
return None
return await self._message_scope(str(message_id).strip())
async def _persist_merged_message(
self,
existing: dict[str, Any],
merged: dict[str, Any],
*,
channel_id: str,
project_id: str,
preserve_timestamp: bool = False,
) -> tuple[dict[str, Any], bool]:
"""Persist a duplicate merge while retaining the mounted cache row.
Semantic transcript duplicates can have a different source message id.
Keeping the existing cache identity (and, for semantic replacement, its
timestamp) upgrades the row in place instead of moving it in the UI.
"""
persisted_id = str(existing.get("message_id", "") or existing.get("id", "") or "")
existing_timestamp = self._message_timestamp(existing)
merged_timestamp = (
existing_timestamp
if preserve_timestamp and existing_timestamp
else self._message_timestamp(merged) or time.time()
)
persisted = {
**merged,
"message_id": persisted_id,
"channel_id": channel_id,
"timestamp": merged_timestamp,
"created_at": merged_timestamp,
}
if self._message_persisted_equal(existing, persisted):
return persisted, False
await self._db.execute(
"UPDATE messages SET sender = ?, sender_name = ?, content = ?, timestamp = ?, "
"reply_to_id = ?, mentions = ?, metadata = ? WHERE message_id = ? AND channel_id = ? AND project_id = ?",
(
persisted["sender"],
persisted["sender_name"],
persisted["content"],
merged_timestamp,
persisted.get("reply_to_id"),
json.dumps(persisted.get("mentions", [])),
json.dumps(persisted.get("metadata", {})),
persisted_id,
channel_id,
project_id,
),
)
return persisted, True
async def _merge_into_same_scope_row(
self,
message_id: str,
@@ -798,13 +873,14 @@ class ChatStore:
channel_id: str,
project_id: str,
candidate: dict[str, Any],
) -> dict[str, Any] | None:
prefer_candidate: bool = False,
) -> tuple[dict[str, Any], bool] | None:
"""Merge ``candidate`` into an already-persisted row with the same id/scope.
Returns the merged row when the update happened (or nothing changed), or
None when the row could not be loaded. Backfill and the live insert path
can race on the same message id; the duplicate must merge in place, never
be re-inserted under a scoped alias id in the same channel.
Returns ``(merged row, materially changed)`` or ``None`` when the row
could not be loaded. Backfill and the live insert path can race on the
same message id; the duplicate must merge in place, never be re-inserted
under a scoped alias id in the same channel.
"""
cursor = await self._db.execute(
"SELECT message_id, channel_id, sender, sender_name, content, "
@@ -816,29 +892,17 @@ class ChatStore:
if row is None:
return None
existing = self._row_to_message_dict(row)
merged = self._merge_duplicate_messages(existing, candidate)
if self._message_persisted_equal(existing, merged):
return merged
merged_timestamp = self._message_timestamp(merged) or time.time()
await self._db.execute(
"UPDATE messages SET sender = ?, sender_name = ?, content = ?, timestamp = ?, "
"reply_to_id = ?, mentions = ?, metadata = ? WHERE message_id = ? AND channel_id = ? AND project_id = ?",
(
merged["sender"],
merged["sender_name"],
merged["content"],
merged_timestamp,
merged.get("reply_to_id"),
json.dumps(merged.get("mentions", [])),
json.dumps(merged.get("metadata", {})),
message_id,
channel_id,
project_id,
),
merged = self._merge_duplicate_messages(
existing,
candidate,
prefer_candidate=prefer_candidate,
)
return await self._persist_merged_message(
existing,
merged,
channel_id=channel_id,
project_id=project_id,
)
merged["timestamp"] = merged_timestamp
merged["created_at"] = merged_timestamp
return merged
async def _allocate_scoped_message_id(
self,
@@ -1581,35 +1645,24 @@ class ChatStore:
existing_index = existing_positions.get(mid)
if existing_index is not None:
existing_match = existing_messages[existing_index]
merged_existing = self._merge_duplicate_messages(existing_match, normalized_message)
if not self._message_persisted_equal(existing_match, merged_existing):
merged_timestamp = self._message_timestamp(merged_existing) or time.time()
await self._db.execute(
"UPDATE messages SET sender = ?, sender_name = ?, content = ?, timestamp = ?, "
"reply_to_id = ?, mentions = ?, metadata = ? WHERE message_id = ? AND channel_id = ? AND project_id = ?",
(
merged_existing["sender"],
merged_existing["sender_name"],
merged_existing["content"],
merged_timestamp,
merged_existing.get("reply_to_id"),
json.dumps(merged_existing.get("mentions", [])),
json.dumps(merged_existing.get("metadata", {})),
mid,
channel_id,
project_id,
),
)
semantic_index.replace(existing_index, {
**merged_existing,
"created_at": merged_timestamp,
})
inserted_messages.append({
**merged_existing,
"channel_id": channel_id,
"timestamp": merged_timestamp,
"created_at": merged_timestamp,
})
# This candidate came directly from the durable transcript.
# For the same identity its original display body is
# authoritative, while metadata unique to the cache is
# still retained by the merge.
merged_existing = self._merge_duplicate_messages(
existing_match,
normalized_message,
prefer_candidate=True,
)
persisted, did_change = await self._persist_merged_message(
existing_match,
merged_existing,
channel_id=channel_id,
project_id=project_id,
)
if did_change:
semantic_index.replace(existing_index, persisted)
inserted_messages.append(persisted)
changed_existing = True
continue
@@ -1623,10 +1676,15 @@ class ChatStore:
channel_id=channel_id,
project_id=project_id,
candidate=normalized_message,
prefer_candidate=True,
)
if merged is not None:
persisted, did_change = merged
existing_ids.add(mid)
existing_positions[mid] = semantic_index.append(merged)
existing_positions[mid] = semantic_index.append(persisted)
if did_change:
inserted_messages.append(persisted)
changed_existing = True
continue
if existing_scope and existing_scope != (channel_id, project_id):
metadata = dict(normalized_message.get("metadata", {}) or {})
@@ -1644,9 +1702,33 @@ class ChatStore:
excluded_message_ids=consumed_existing_ids,
)
if duplicate_index is not None:
consumed_existing_ids.add(
existing_messages[duplicate_index]["message_id"]
existing_duplicate = existing_messages[duplicate_index]
existing_id = str(existing_duplicate["message_id"])
# Prefer the authoritative transcript on an equal score, but
# do not replace a stronger canonical result surface with a
# lower-priority mirror. The existing cache id and timestamp
# remain stable either way.
prefer_candidate = (
self._message_preference_score(normalized_message)
>= self._message_preference_score(existing_duplicate)
)
merged_duplicate = self._merge_duplicate_messages(
existing_duplicate,
normalized_message,
prefer_candidate=prefer_candidate,
)
persisted, did_change = await self._persist_merged_message(
existing_duplicate,
merged_duplicate,
channel_id=channel_id,
project_id=project_id,
preserve_timestamp=True,
)
if did_change:
semantic_index.replace(duplicate_index, persisted)
inserted_messages.append(persisted)
changed_existing = True
consumed_existing_ids.add(existing_id)
continue
try:
@@ -1674,11 +1756,16 @@ class ChatStore:
channel_id=channel_id,
project_id=project_id,
candidate=normalized_message,
prefer_candidate=True,
)
if merged is not None:
persisted, did_change = merged
merged_id = normalized_message["message_id"]
existing_ids.add(merged_id)
existing_positions[merged_id] = semantic_index.append(merged)
existing_positions[merged_id] = semantic_index.append(persisted)
if did_change:
inserted_messages.append(persisted)
changed_existing = True
continue
metadata = dict(normalized_message.get("metadata", {}) or {})
metadata.setdefault("ui_message_id", normalized_message["message_id"])
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:," />
<title>OpenOPC Pixel Office</title>
<script type="module" crossorigin src="./assets/index-Cson66Y4.js"></script>
<script type="module" crossorigin src="./assets/index-CTK-c7_K.js"></script>
<link rel="modulepreload" crossorigin href="./assets/phaser-DFK5Ua9d.js">
<link rel="stylesheet" crossorigin href="./assets/index-DEvLDWDw.css">
</head>
@@ -127,16 +127,37 @@ assert.match(
'runtime events carrying a non-empty tool_name must update stable displayTool (empty tool_name keeps the sticky last command)',
)
// 8. Assistant streaming drafts must disappear at real terminal boundaries.
// 8. A streaming draft is one mounted logical turn. Completion/checkpoint
// events cannot remove it before the matching persisted final is merged.
assert.match(
src,
/evt\.type === 'turn_completed' \|\| evt\.type === 'turn_failed' \|\| evt\.type === 'checkpoint_saved'/,
'runtime terminal/checkpoint events must clear task-mode Live Reply drafts',
/const turnId = resolveCanonicalTurnId\(data\) \|\| undefined/,
'runtime deltas must use the shared canonical-turn resolver',
)
assert.match(
src,
/detailHasFinalForDraft[\s\S]*runtime_v2_assistant[\s\S]*ss\.clearDraft\(detailTaskId\)/,
'session_detail backfill of the final runtime assistant turn must clear matching Live Reply drafts',
/const startsNewCanonicalTurn = evt\.type === 'turn_started'[\s\S]*turnId !== activeDraftTurnId[\s\S]*evt\.type === 'turn_failed' \|\| startsNewCanonicalTurn/,
'only failure or a genuinely new canonical turn may clear an uncommitted draft',
)
assert.doesNotMatch(
src,
/evt\.type === 'turn_completed'[^\n]*clearDraft|evt\.type === 'checkpoint_saved'[^\n]*clearDraft/,
'turn completion and checkpoint persistence must not clear a draft ahead of its final message',
)
assert.match(
src,
/detailHasFinalForDraft[\s\S]*terminalAssistantTurnId\(message\) === draftTurnId[\s\S]*ss\.clearDraft\(detailTaskId\)/,
'session_detail may clear only after merging the matching terminal assistant turn',
)
assert.match(
src,
/cs\.addMessageFromBackend\(mapped\)[\s\S]*terminalTurnId = terminalAssistantTurnId\(mapped\)[\s\S]*terminalTurnId === activeDraftTurnId[\s\S]*clearDraft\(taskId\)/,
'session_message/chat_new_message must merge a matching terminal final before clearing its draft',
)
assert.doesNotMatch(
src,
/mapped\.sender !== 'user'\)\s*\{\s*sessionStoreRef\.current\?\.clearDraft/,
'an unrelated non-user session message must never clear an active draft',
)
// 9. Summary/full pagination and transport-local failures have independent
+27 -19
View File
@@ -24,6 +24,7 @@ import { companyRuntimeControlPatchForBoardStatus } from './lib/sessionRuntime'
import { getExecutionTurnId } from './lib/workItemRuntimeIds'
import { normalizeSessionCompanyProfile, normalizeSessionExecMode } from './lib/sessionIdentity'
import { extractSessionRecruitmentByRole, sessionChannelId } from './lib/sessionRecruitment'
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './lib/turnIdentity'
import { unassignAgent } from './game/map/OfficeStore'
import type { AgentAnimStatus, EmployeeAssignment, KanbanPhase, KanbanTask, RoleAggregatedStatus, RoleWorkItemSummary, Session, TaskPreferredAgent } from './types/kanban'
@@ -919,9 +920,11 @@ export default function App() {
const taskId = typeof data.task_id === 'string' ? data.task_id : ''
if (taskId) {
const isDeltaEvent = evt.type === 'assistant_delta' || evt.type === 'thinking_delta'
const bufferedDraftTurnId = pendingDeltaFlushRef.current.get(taskId)?.draftTurnId
// Store-write ordering guarantee: everything buffered for this
// task lands before a non-delta event (turn boundaries call
// clearDraft; the draft must be flushed first, not after).
// task lands before a non-delta event. A matching terminal
// message replaces the draft later; flushing here must not make
// a completion boundary remove the visible turn prematurely.
if (!isDeltaEvent) flushPendingDeltas(taskId)
const ss = sessionStoreRef.current
const bs = boardStoreRef.current
@@ -931,13 +934,10 @@ export default function App() {
: ''
const executionMode = typeof data.execution_mode === 'string' ? data.execution_mode : ''
const isTaskModeRuntime = executionMode === 'task_mode' || projectionId === 'task_mode_execution'
const turnId = typeof data.turn_id === 'string' && data.turn_id
? data.turn_id
: typeof data.canonical_turn_id === 'string' && data.canonical_turn_id
? data.canonical_turn_id
: typeof data.execution_turn_id === 'string' && data.execution_turn_id
? data.execution_turn_id
: undefined
// Drafts represent one logical assistant turn. The shared
// resolver deliberately prefers conversation identity over the
// iteration-scoped runtime turn id.
const turnId = resolveCanonicalTurnId(data) || undefined
const marksCompanyRuntime =
!!projectionId && projectionId !== 'task_mode_execution' && !isTaskModeRuntime
if (marksCompanyRuntime && !isDeltaEvent && existingSession?.isCompanyRuntime !== true) {
@@ -993,7 +993,14 @@ export default function App() {
entry.kanbanPatch = { ...entry.kanbanPatch, ...kanbanPatch }
scheduleDeltaFlush()
} else {
if (evt.type === 'turn_started' || evt.type === 'turn_completed' || evt.type === 'turn_failed' || evt.type === 'checkpoint_saved') {
const activeDraftTurnId = String(
bufferedDraftTurnId ?? existingSession?.draftTurnId ?? '',
).trim()
const startsNewCanonicalTurn = evt.type === 'turn_started'
&& !!turnId
&& !!activeDraftTurnId
&& turnId !== activeDraftTurnId
if (evt.type === 'turn_failed' || startsNewCanonicalTurn) {
ss?.clearDraft(taskId)
}
ss?.updateSession(taskId, runtimePartial)
@@ -1084,14 +1091,9 @@ export default function App() {
payload.client_history_page === true,
)
const draftTurnId = String(existingSession?.draftTurnId ?? '').trim()
const detailHasFinalForDraft = !!draftTurnId && detailMessages.some((message) => {
if (message.sender === 'user') return false
const metadata = message.metadata ?? {}
const transcriptKind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
if (transcriptKind !== 'runtime_v2_assistant') return false
const messageTurnId = String(metadata.canonical_turn_id ?? metadata.turn_id ?? '').trim()
return messageTurnId === draftTurnId
})
const detailHasFinalForDraft = !!draftTurnId
&& !!cs
&& detailMessages.some(message => terminalAssistantTurnId(message) === draftTurnId)
if (detailHasFinalForDraft) {
ss.clearDraft(detailTaskId)
}
@@ -1672,8 +1674,14 @@ export default function App() {
console.debug('[onSessionMessage]', mapped.sender, mapped.channelId, mapped.content?.slice(0, 60))
cs.addMessageFromBackend(mapped)
const taskId = mapped.channelId.startsWith('session:') ? mapped.channelId.slice('session:'.length) : ''
const terminalTurnId = terminalAssistantTurnId(mapped)
const activeDraftTurnId = String(
sessionStoreRef.current?.sessions.find(session => session.taskId === taskId)?.draftTurnId ?? '',
).trim()
if (taskId && mapped.sender !== 'user') {
sessionStoreRef.current?.clearDraft(taskId)
if (terminalTurnId && terminalTurnId === activeDraftTurnId) {
sessionStoreRef.current?.clearDraft(taskId)
}
// Force refresh — session messages are critical content that must sync
scheduleSessionDetailRefresh(taskId, 'full', true)
}
@@ -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,
@@ -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])
}
@@ -472,7 +472,7 @@ async function runTimelineIdentityCases(page: Page, baseUrl: string): Promise<vo
collapsed: !!committed.querySelector('.msg-collapse-toggle'),
}
})
assert.equal(finalizedDraft.reused, true, 'draft -> runtime_v2_assistant final must reuse the same outer timeline DOM node')
assert.equal(finalizedDraft.reused, true, 'draft -> runtime_v2_company_assistant final must reuse the same outer timeline DOM node')
assert.equal(finalizedDraft.collapsed, false, 'a mounted expanded draft must not auto-collapse when its final arrives')
assert.ok(
finalizedDraft.height >= draftGeometry.height - 4,
@@ -366,7 +366,8 @@ function Fixture() {
mentions: [],
metadata: {
canonical_turn_id: LIVE_TURN_ID,
transcript_kind: 'runtime_v2_assistant',
result_delivery_id: `result:${LIVE_TURN_ID}:attempt:0`,
transcript_kind: 'runtime_v2_company_assistant',
},
},
])
@@ -341,7 +341,13 @@ export interface ChatMessageMeta {
/** UI-only identity retained across semantic result-surface replacement. */
ui_timeline_id?: string
canonical_turn_id?: string
conversation_turn_id?: string
turn_id?: string
execution_turn_id?: string
result_delivery_id?: string
source_result_message_id?: string
source_task_id?: string
child_session_id?: string
execution_mode?: string
transcript_kind?: string
detail_visibility?: DetailVisibility
+40 -26
View File
@@ -192,23 +192,33 @@ def _normalize_duplicate_content(content: Any) -> str:
def _strip_narrative_title_prefix(content: str) -> str:
trimmed = str(content or "").strip()
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
if markdown_title:
# Comparison-only canonicalization may unwrap a deliberate leading title,
# but must never interpret a colon inside ordinary Markdown body text as a
# wrapper boundary. Resolve explicit nested wrappers to a fixed point so
# the comparison key itself is idempotent.
while True:
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
if not markdown_title:
return trimmed
body = markdown_title.group(2).strip()
if len(body) >= 80:
return body
colon_index = trimmed.find(": ")
if colon_index < 8 or colon_index > 160:
return trimmed
prefix = trimmed[:colon_index].replace("*", "").strip()
body = trimmed[colon_index + 2 :].strip()
if len(body) < 80:
return trimmed
if not re.search(r"[A-Za-z\u4e00-\u9fff]", prefix):
return trimmed
if re.match(r"^(https?|file)$", prefix, flags=re.IGNORECASE):
return trimmed
return body
if len(body) < 80 or body == trimmed:
return trimmed
trimmed = body
def _select_duplicate_display_content(
preferred: dict[str, Any],
secondary: dict[str, Any],
) -> str:
"""Return one original surface body, never the lossy comparison key."""
preferred_content = str(preferred.get("content", "") or "")
secondary_content = str(secondary.get("content", "") or "")
preferred_key = _normalize_duplicate_content(preferred_content)
if not preferred_key or preferred_key != _normalize_duplicate_content(secondary_content):
return preferred_content
if secondary_content.strip() == preferred_key and preferred_content.strip() != preferred_key:
return secondary_content
return preferred_content
def _normalize_transcript_detail_level(value: Any) -> TranscriptDetailLevel:
@@ -1132,7 +1142,19 @@ def _transcript_item_to_ui_message(
**({"runtime_thinking": runtime_thinking} if runtime_thinking else {}),
**({
key: message_metadata.get(key)
for key in ("canonical_turn_id", "turn_id")
for key in (
"canonical_turn_id",
"turn_id",
"result_delivery_id",
"source_result_message_id",
"source_task_id",
"child_session_id",
"conversation_turn_id",
"execution_turn_id",
"work_item_projection_id",
"work_item_turn_type",
"runtime_session_id",
)
if message_metadata.get(key)
}),
**ui_meta,
@@ -1193,18 +1215,10 @@ def collapse_adjacent_transcript_duplicates(messages: list[dict[str, Any]]) -> l
):
merged_visibility = "full"
merged_metadata["detail_visibility"] = merged_visibility
preferred_content = str(preferred.get("content", "") or "")
secondary_content = str(secondary.get("content", "") or "")
normalized_content = _normalize_duplicate_content(preferred_content)
merged_content = (
normalized_content
if normalized_content and normalized_content == _normalize_duplicate_content(secondary_content)
else preferred_content
)
collapsed[-1] = {
**secondary,
**preferred,
"content": merged_content,
"content": _select_duplicate_display_content(preferred, secondary),
"metadata": merged_metadata,
}
return collapsed