Initial commit
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
import type { ChatMessage } from '../types/chat'
|
||||
import type { ProgressEntry, Session } from '../types/kanban'
|
||||
import { getContextUsageMetrics } from './contextUsage'
|
||||
import { isSessionWorking } from './sessionRuntime'
|
||||
|
||||
const CONTEXT_TOKENS_RE = /(\d[\d,]*)\s*\/\s*(\d[\d,]*)\s+tokens/i
|
||||
const USED_PCT_RE = /(\d{1,3})%\s*used/i
|
||||
const REMAINING_PCT_RE = /(\d{1,3})%\s*remaining/i
|
||||
|
||||
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 resultSurfacePriority(message: ChatMessage): number {
|
||||
const meta = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const transcriptKind = String(meta.transcript_kind ?? meta.kind ?? '').trim()
|
||||
switch (transcriptKind) {
|
||||
case 'child_task_result':
|
||||
return 80
|
||||
case 'child_task_result_retry':
|
||||
return 79
|
||||
case 'company_role_result':
|
||||
return 75
|
||||
case 'company_role_result_retry':
|
||||
return 74
|
||||
case 'child_result':
|
||||
return 70
|
||||
case 'runtime_v2_assistant':
|
||||
return 60
|
||||
case 'runtime_v2_company_assistant':
|
||||
return 20
|
||||
case 'top_level_reply':
|
||||
return 40
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (String(meta.kind ?? '').trim() === 'worker_notification') {
|
||||
return 20
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function resultSurfaceDedupeKey(message: ChatMessage): string {
|
||||
if (resultSurfacePriority(message) <= 0) return ''
|
||||
const content = compactWhitespace(stripNarrativeTitlePrefix(message.content)).slice(0, 2000)
|
||||
return content ? `result:${content}` : ''
|
||||
}
|
||||
|
||||
function parseProgressNumber(value: string | undefined): number | undefined {
|
||||
if (!value) return undefined
|
||||
const normalized = value.replace(/,/g, '').trim()
|
||||
if (!normalized) return undefined
|
||||
const parsed = Number(normalized)
|
||||
return Number.isFinite(parsed) ? Math.max(0, Math.round(parsed)) : undefined
|
||||
}
|
||||
|
||||
function clampPct(value: number | undefined): number | undefined {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return undefined
|
||||
return Math.max(0, Math.min(Math.round(value), 100))
|
||||
}
|
||||
|
||||
export function deriveCompanyRuntimeDisplayStatus(session: Session | null | undefined): string | undefined {
|
||||
if (!session) return undefined
|
||||
const execMode = String(session.execMode ?? '').trim()
|
||||
const isCompanyLike = (
|
||||
execMode === 'company'
|
||||
|| execMode === 'org'
|
||||
|| execMode === 'custom'
|
||||
|| !!session.isCompanyRuntime
|
||||
|| !!session.roleWorkItems
|
||||
|| !!session.executorRoleWorkItems
|
||||
)
|
||||
if (!isCompanyLike) return undefined
|
||||
const summaries = Object.values(session.executorRoleWorkItems ?? session.roleWorkItems ?? {})
|
||||
if (summaries.length === 0) return undefined
|
||||
const statuses = summaries.map(summary => summary.aggregatedStatus)
|
||||
if (statuses.some(status => status === 'active')) return 'running'
|
||||
if (statuses.some(status => status === 'waiting')) return 'pending'
|
||||
if (statuses.some(status => status === 'pending')) return 'pending'
|
||||
if (statuses.every(status => status === 'failed')) return 'failed'
|
||||
if (statuses.every(status => status === 'done')) return 'done'
|
||||
if (statuses.some(status => status === 'done')) return 'done'
|
||||
return undefined
|
||||
}
|
||||
|
||||
function deriveContextFromProgressLog(progressLog: ProgressEntry[] | undefined): {
|
||||
contextTokens?: number
|
||||
contextWindow?: number
|
||||
contextRemainingPct?: number
|
||||
} {
|
||||
const derived: {
|
||||
contextTokens?: number
|
||||
contextWindow?: number
|
||||
contextRemainingPct?: number
|
||||
} = {}
|
||||
|
||||
for (const entry of progressLog ?? []) {
|
||||
const text = `${entry.summary ?? ''} ${entry.detail ?? ''}`.trim()
|
||||
if (!text) continue
|
||||
|
||||
const tokenMatch = CONTEXT_TOKENS_RE.exec(text)
|
||||
if (tokenMatch) {
|
||||
const usedTokens = parseProgressNumber(tokenMatch[1])
|
||||
const windowTokens = parseProgressNumber(tokenMatch[2])
|
||||
if (typeof usedTokens === 'number') derived.contextTokens = usedTokens
|
||||
if (typeof windowTokens === 'number' && windowTokens > 0) derived.contextWindow = windowTokens
|
||||
}
|
||||
|
||||
const remainingMatch = REMAINING_PCT_RE.exec(text)
|
||||
if (remainingMatch) {
|
||||
derived.contextRemainingPct = clampPct(parseProgressNumber(remainingMatch[1]))
|
||||
continue
|
||||
}
|
||||
|
||||
const usedMatch = USED_PCT_RE.exec(text)
|
||||
if (usedMatch) {
|
||||
const usedPct = clampPct(parseProgressNumber(usedMatch[1]))
|
||||
if (typeof usedPct === 'number') derived.contextRemainingPct = 100 - usedPct
|
||||
}
|
||||
}
|
||||
|
||||
return derived
|
||||
}
|
||||
|
||||
function withDerivedSessionRuntime(session: Session): Session {
|
||||
const derivedContext = deriveContextFromProgressLog(session.progressLog)
|
||||
const contextTokens = session.contextTokens ?? derivedContext.contextTokens
|
||||
const contextWindow = session.contextWindow ?? derivedContext.contextWindow
|
||||
const contextRemainingPct = session.contextRemainingPct ?? derivedContext.contextRemainingPct
|
||||
|
||||
if (
|
||||
contextTokens === session.contextTokens
|
||||
&& contextWindow === session.contextWindow
|
||||
&& contextRemainingPct === session.contextRemainingPct
|
||||
) {
|
||||
return session
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
contextTokens,
|
||||
contextWindow,
|
||||
contextRemainingPct,
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueSessionsByTaskId(sessions: Session[]): Session[] {
|
||||
const seen = new Set<string>()
|
||||
const unique: Session[] = []
|
||||
for (const session of sessions) {
|
||||
if (seen.has(session.taskId)) continue
|
||||
seen.add(session.taskId)
|
||||
unique.push(session)
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
function hasWorkItemRoleIdentity(session: Session | null | undefined): boolean {
|
||||
return !!(
|
||||
String(session?.workItemRoleId ?? '').trim()
|
||||
|| String(session?.workItemRoleName ?? '').trim()
|
||||
)
|
||||
}
|
||||
|
||||
export function getWorkItemChildSessions(activeSession: Session | null, sessions: Session[]): Session[] {
|
||||
if (!activeSession || activeSession.mode === 'child') return []
|
||||
|
||||
const parentKeys = new Set<string>()
|
||||
if (activeSession.sessionId) parentKeys.add(activeSession.sessionId)
|
||||
parentKeys.add(activeSession.taskId)
|
||||
const activeOriginTaskId = String(activeSession.originTaskId ?? activeSession.taskId ?? '').trim()
|
||||
|
||||
const executionTurnOrder = new Map<string, number>()
|
||||
for (const entry of activeSession.workItemLog ?? []) {
|
||||
const taskId = entry.executionTurnId || entry.runtimeTaskId
|
||||
if (taskId && !executionTurnOrder.has(taskId)) {
|
||||
executionTurnOrder.set(taskId, executionTurnOrder.size)
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set<string>()
|
||||
const matches = sessions.filter((session) => {
|
||||
if (session.taskId === activeSession.taskId) return false
|
||||
if (seen.has(session.taskId)) return false
|
||||
|
||||
const linkedByParent = !!session.parentSessionId && parentKeys.has(session.parentSessionId)
|
||||
const linkedByWorkItem = executionTurnOrder.has(session.taskId)
|
||||
const linkedByOrigin = !!activeOriginTaskId && String(session.originTaskId ?? '').trim() === activeOriginTaskId
|
||||
if (!linkedByParent && !linkedByWorkItem && !linkedByOrigin) return false
|
||||
|
||||
seen.add(session.taskId)
|
||||
return true
|
||||
})
|
||||
|
||||
return matches.sort((a, b) => {
|
||||
const aOrder = executionTurnOrder.get(a.taskId)
|
||||
const bOrder = executionTurnOrder.get(b.taskId)
|
||||
if (aOrder != null && bOrder != null && aOrder !== bOrder) return aOrder - bOrder
|
||||
if (aOrder != null && bOrder == null) return -1
|
||||
if (aOrder == null && bOrder != null) return 1
|
||||
return b.updatedAt - a.updatedAt
|
||||
})
|
||||
}
|
||||
|
||||
export function getWorkItemRoleSessions(activeSession: Session | null, sessions: Session[]): Session[] {
|
||||
const childSessions = getWorkItemChildSessions(activeSession, sessions)
|
||||
if (!activeSession || activeSession.mode === 'child') return childSessions
|
||||
if (!hasWorkItemRoleIdentity(activeSession)) return childSessions
|
||||
|
||||
return uniqueSessionsByTaskId([activeSession, ...childSessions])
|
||||
}
|
||||
|
||||
export function getConversationPeerSessions(activeSession: Session | null, sessions: Session[]): Session[] {
|
||||
if (!activeSession || activeSession.mode === 'child') return []
|
||||
const activeSessionId = String(activeSession.sessionId ?? '').trim()
|
||||
if (!activeSessionId) return []
|
||||
|
||||
const seen = new Set<string>()
|
||||
return sessions.filter((session) => {
|
||||
if (session.taskId === activeSession.taskId) return false
|
||||
if (seen.has(session.taskId)) return false
|
||||
if (String(session.sessionId ?? '').trim() !== activeSessionId) return false
|
||||
seen.add(session.taskId)
|
||||
return true
|
||||
}).sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
function isSummaryConversationSession(session: Session | null | undefined): boolean {
|
||||
const execMode = String(session?.execMode ?? '').trim()
|
||||
return execMode === 'company' || execMode === 'org' || execMode === 'custom'
|
||||
}
|
||||
|
||||
function projectedDisplayScore(session: Session): number {
|
||||
const workingBonus = isSessionWorking(session) ? 1_000_000_000 : 0
|
||||
const draftBonus = String(session.draftAssistantText ?? '').trim() ? 10_000_000_000 : 0
|
||||
const messageBonus = Math.max(0, session.messageCount ?? 0) * 10_000
|
||||
const progressBonus = (session.progressLog?.length ?? 0) * 100
|
||||
return draftBonus + workingBonus + messageBonus + progressBonus + session.updatedAt
|
||||
}
|
||||
|
||||
function runtimeProjectionScore(session: Session): number {
|
||||
const contextUsage = getContextUsageMetrics(session)
|
||||
const workingBonus = isSessionWorking(session) ? 100_000_000_000 : 0
|
||||
const draftBonus = String(session.draftAssistantText ?? '').trim() ? 10_000_000_000 : 0
|
||||
const contextBonus = (
|
||||
typeof contextUsage.usedPct === 'number'
|
||||
|| typeof contextUsage.usedTokens === 'number'
|
||||
|| typeof contextUsage.windowTokens === 'number'
|
||||
) ? 10_000_000_000 : 0
|
||||
const tokenBonus = (
|
||||
typeof session.inputTokens === 'number'
|
||||
|| typeof session.outputTokens === 'number'
|
||||
|| typeof session.totalTokens === 'number'
|
||||
) ? 1_000_000_000 : 0
|
||||
const messageBonus = Math.max(0, session.messageCount ?? 0) * 10_000
|
||||
const progressBonus = (session.progressLog?.length ?? 0) * 100
|
||||
return workingBonus + draftBonus + contextBonus + tokenBonus + messageBonus + progressBonus
|
||||
}
|
||||
|
||||
export interface SessionConversationProjection {
|
||||
timelineSessions: Session[]
|
||||
displaySession: Session | null
|
||||
runtimeSession: Session | null
|
||||
projectedFromChild: boolean
|
||||
}
|
||||
|
||||
export function projectSessionConversation(
|
||||
activeSession: Session | null,
|
||||
relatedSessions: Session[],
|
||||
): SessionConversationProjection {
|
||||
if (!activeSession) {
|
||||
return {
|
||||
timelineSessions: [],
|
||||
displaySession: null,
|
||||
runtimeSession: null,
|
||||
projectedFromChild: false,
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedActiveSession = withDerivedSessionRuntime(activeSession)
|
||||
if (activeSession.mode === 'child' || relatedSessions.length === 0) {
|
||||
return {
|
||||
timelineSessions: [normalizedActiveSession],
|
||||
displaySession: normalizedActiveSession,
|
||||
runtimeSession: normalizedActiveSession,
|
||||
projectedFromChild: false,
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRelatedSessions = relatedSessions.map(withDerivedSessionRuntime)
|
||||
const visibleRelatedSessions = normalizedRelatedSessions.filter((session) => session.status !== 'cancelled')
|
||||
const projectedSessions = visibleRelatedSessions.length > 0 ? visibleRelatedSessions : normalizedRelatedSessions
|
||||
const timelineSessions = uniqueSessionsByTaskId([normalizedActiveSession, ...projectedSessions])
|
||||
const summaryConversation = isSummaryConversationSession(activeSession)
|
||||
const projectedDisplaySession = [...timelineSessions].sort(
|
||||
(a, b) => projectedDisplayScore(b) - projectedDisplayScore(a),
|
||||
)[0] ?? normalizedActiveSession
|
||||
const displaySession = summaryConversation
|
||||
? normalizedActiveSession
|
||||
: projectedDisplaySession
|
||||
const runtimeSession = [...timelineSessions].sort(
|
||||
(a, b) => runtimeProjectionScore(b) - runtimeProjectionScore(a),
|
||||
)[0] ?? displaySession
|
||||
|
||||
return {
|
||||
timelineSessions,
|
||||
displaySession,
|
||||
runtimeSession,
|
||||
projectedFromChild: !summaryConversation && displaySession.taskId !== activeSession.taskId,
|
||||
}
|
||||
}
|
||||
|
||||
export function getConversationSessionView(
|
||||
activeSession: Session | null,
|
||||
runtimeSession: Session | null,
|
||||
timelineSessions: Session[],
|
||||
): Session | null {
|
||||
if (!activeSession) return null
|
||||
const normalizedActiveSession = withDerivedSessionRuntime(activeSession)
|
||||
const runtimeSource = runtimeSession ?? normalizedActiveSession
|
||||
const companyDisplayStatus = deriveCompanyRuntimeDisplayStatus(runtimeSource)
|
||||
?? deriveCompanyRuntimeDisplayStatus(normalizedActiveSession)
|
||||
const mergedMessageCount = getConversationMessageCount(
|
||||
timelineSessions.length > 0 ? timelineSessions : [normalizedActiveSession],
|
||||
)
|
||||
|
||||
return {
|
||||
...normalizedActiveSession,
|
||||
status: companyDisplayStatus ?? (runtimeSource.status || normalizedActiveSession.status),
|
||||
assigneeIds: runtimeSource.assigneeIds.length > 0
|
||||
? runtimeSource.assigneeIds
|
||||
: normalizedActiveSession.assigneeIds,
|
||||
agentStatus: runtimeSource.agentStatus ?? normalizedActiveSession.agentStatus,
|
||||
currentTool: runtimeSource.currentTool ?? normalizedActiveSession.currentTool,
|
||||
displayTool: runtimeSource.displayTool ?? runtimeSource.currentTool ?? normalizedActiveSession.displayTool ?? normalizedActiveSession.currentTool,
|
||||
toolElapsedMs: runtimeSource.toolElapsedMs ?? normalizedActiveSession.toolElapsedMs,
|
||||
lastToolSummary: runtimeSource.lastToolSummary ?? normalizedActiveSession.lastToolSummary,
|
||||
contextTokens: runtimeSource.contextTokens ?? normalizedActiveSession.contextTokens,
|
||||
contextWindow: runtimeSource.contextWindow ?? normalizedActiveSession.contextWindow,
|
||||
contextRemainingPct: runtimeSource.contextRemainingPct ?? normalizedActiveSession.contextRemainingPct,
|
||||
inputTokens: runtimeSource.inputTokens ?? normalizedActiveSession.inputTokens,
|
||||
outputTokens: runtimeSource.outputTokens ?? normalizedActiveSession.outputTokens,
|
||||
totalTokens: runtimeSource.totalTokens ?? normalizedActiveSession.totalTokens,
|
||||
turnCostUsd: runtimeSource.turnCostUsd ?? normalizedActiveSession.turnCostUsd,
|
||||
sessionCostUsd: runtimeSource.sessionCostUsd ?? normalizedActiveSession.sessionCostUsd,
|
||||
pendingPermissionCount: runtimeSource.pendingPermissionCount ?? normalizedActiveSession.pendingPermissionCount,
|
||||
drainMode: runtimeSource.drainMode ?? normalizedActiveSession.drainMode,
|
||||
workItemProjectionId: runtimeSource.workItemProjectionId ?? normalizedActiveSession.workItemProjectionId,
|
||||
workItemTurnType: runtimeSource.workItemTurnType ?? normalizedActiveSession.workItemTurnType,
|
||||
companyProfile: normalizedActiveSession.companyProfile ?? runtimeSource.companyProfile,
|
||||
workItemRoleId: normalizedActiveSession.workItemRoleId ?? runtimeSource.workItemRoleId,
|
||||
workItemRoleName: normalizedActiveSession.workItemRoleName ?? runtimeSource.workItemRoleName,
|
||||
workItemGate: normalizedActiveSession.workItemGate ?? runtimeSource.workItemGate,
|
||||
employeeAssignment: normalizedActiveSession.employeeAssignment ?? runtimeSource.employeeAssignment,
|
||||
selectedExecutionAgent: runtimeSource.selectedExecutionAgent ?? normalizedActiveSession.selectedExecutionAgent,
|
||||
draftAssistantText: runtimeSource.draftAssistantText ?? normalizedActiveSession.draftAssistantText,
|
||||
draftUpdatedAt: runtimeSource.draftUpdatedAt ?? normalizedActiveSession.draftUpdatedAt,
|
||||
draftIteration: runtimeSource.draftIteration ?? normalizedActiveSession.draftIteration,
|
||||
draftTurnId: runtimeSource.draftTurnId ?? normalizedActiveSession.draftTurnId,
|
||||
runtimeControlState: runtimeSource.runtimeControlState ?? normalizedActiveSession.runtimeControlState,
|
||||
canStop: runtimeSource.canStop ?? normalizedActiveSession.canStop,
|
||||
canResume: runtimeSource.canResume ?? normalizedActiveSession.canResume,
|
||||
resumeParentTaskId: runtimeSource.resumeParentTaskId ?? normalizedActiveSession.resumeParentTaskId,
|
||||
resumeParentSessionId: runtimeSource.resumeParentSessionId ?? normalizedActiveSession.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: runtimeSource.pendingRuntimeCheckpointId ?? normalizedActiveSession.pendingRuntimeCheckpointId,
|
||||
stopIntentId: runtimeSource.stopIntentId ?? normalizedActiveSession.stopIntentId,
|
||||
updatedAt: Math.max(
|
||||
normalizedActiveSession.updatedAt,
|
||||
runtimeSource.updatedAt,
|
||||
),
|
||||
messageCount: Math.max(
|
||||
normalizedActiveSession.messageCount ?? 0,
|
||||
runtimeSource.messageCount ?? 0,
|
||||
mergedMessageCount,
|
||||
),
|
||||
isCompanyRuntime: !!(normalizedActiveSession.isCompanyRuntime || runtimeSource.isCompanyRuntime),
|
||||
workItemLog: (normalizedActiveSession.workItemLog?.length ?? 0) > 0
|
||||
? normalizedActiveSession.workItemLog
|
||||
: runtimeSource.workItemLog,
|
||||
roleWorkItems: normalizedActiveSession.roleWorkItems ?? runtimeSource.roleWorkItems,
|
||||
executorRoleWorkItems: normalizedActiveSession.executorRoleWorkItems ?? runtimeSource.executorRoleWorkItems,
|
||||
activeSubagents: normalizedActiveSession.activeSubagents ?? runtimeSource.activeSubagents,
|
||||
permissionRequests: normalizedActiveSession.permissionRequests ?? runtimeSource.permissionRequests,
|
||||
}
|
||||
}
|
||||
|
||||
export function getConversationHeaderSession(
|
||||
activeSession: Session | null,
|
||||
runtimeSession: Session | null,
|
||||
timelineSessions: Session[],
|
||||
): Session | null {
|
||||
if (!activeSession) return null
|
||||
const normalizedActiveSession = withDerivedSessionRuntime(activeSession)
|
||||
const runtimeSource = runtimeSession ?? normalizedActiveSession
|
||||
const companyDisplayStatus = deriveCompanyRuntimeDisplayStatus(runtimeSource)
|
||||
?? deriveCompanyRuntimeDisplayStatus(normalizedActiveSession)
|
||||
const mergedMessageCount = getConversationMessageCount(
|
||||
timelineSessions.length > 0 ? timelineSessions : [normalizedActiveSession],
|
||||
)
|
||||
|
||||
return {
|
||||
...normalizedActiveSession,
|
||||
status: companyDisplayStatus ?? (runtimeSource.status || normalizedActiveSession.status),
|
||||
agentStatus: runtimeSource.agentStatus ?? normalizedActiveSession.agentStatus,
|
||||
currentTool: runtimeSource.currentTool ?? normalizedActiveSession.currentTool,
|
||||
displayTool: runtimeSource.displayTool ?? runtimeSource.currentTool ?? normalizedActiveSession.displayTool ?? normalizedActiveSession.currentTool,
|
||||
toolElapsedMs: runtimeSource.toolElapsedMs ?? normalizedActiveSession.toolElapsedMs,
|
||||
lastToolSummary: runtimeSource.lastToolSummary ?? normalizedActiveSession.lastToolSummary,
|
||||
contextTokens: runtimeSource.contextTokens ?? normalizedActiveSession.contextTokens,
|
||||
contextWindow: runtimeSource.contextWindow ?? normalizedActiveSession.contextWindow,
|
||||
contextRemainingPct: runtimeSource.contextRemainingPct ?? normalizedActiveSession.contextRemainingPct,
|
||||
inputTokens: runtimeSource.inputTokens ?? normalizedActiveSession.inputTokens,
|
||||
outputTokens: runtimeSource.outputTokens ?? normalizedActiveSession.outputTokens,
|
||||
totalTokens: runtimeSource.totalTokens ?? normalizedActiveSession.totalTokens,
|
||||
turnCostUsd: runtimeSource.turnCostUsd ?? normalizedActiveSession.turnCostUsd,
|
||||
sessionCostUsd: runtimeSource.sessionCostUsd ?? normalizedActiveSession.sessionCostUsd,
|
||||
pendingPermissionCount: runtimeSource.pendingPermissionCount ?? normalizedActiveSession.pendingPermissionCount,
|
||||
drainMode: runtimeSource.drainMode ?? normalizedActiveSession.drainMode,
|
||||
selectedExecutionAgent: runtimeSource.selectedExecutionAgent ?? normalizedActiveSession.selectedExecutionAgent,
|
||||
runtimeControlState: runtimeSource.runtimeControlState ?? normalizedActiveSession.runtimeControlState,
|
||||
canStop: runtimeSource.canStop ?? normalizedActiveSession.canStop,
|
||||
canResume: runtimeSource.canResume ?? normalizedActiveSession.canResume,
|
||||
resumeParentTaskId: runtimeSource.resumeParentTaskId ?? normalizedActiveSession.resumeParentTaskId,
|
||||
resumeParentSessionId: runtimeSource.resumeParentSessionId ?? normalizedActiveSession.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: runtimeSource.pendingRuntimeCheckpointId ?? normalizedActiveSession.pendingRuntimeCheckpointId,
|
||||
stopIntentId: runtimeSource.stopIntentId ?? normalizedActiveSession.stopIntentId,
|
||||
updatedAt: Math.max(normalizedActiveSession.updatedAt, runtimeSource.updatedAt),
|
||||
messageCount: Math.max(
|
||||
normalizedActiveSession.messageCount ?? 0,
|
||||
runtimeSource.messageCount ?? 0,
|
||||
mergedMessageCount,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeConversationMessages(messageGroups: ChatMessage[][]): ChatMessage[] {
|
||||
const seen = new Set<string>()
|
||||
const resultKeyIndex = new Map<string, number>()
|
||||
const merged: ChatMessage[] = []
|
||||
for (const group of messageGroups) {
|
||||
for (const message of group) {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const uiMessageId = typeof metadata.ui_message_id === 'string'
|
||||
? metadata.ui_message_id.trim()
|
||||
: ''
|
||||
const resultKey = resultSurfaceDedupeKey(message)
|
||||
if (resultKey) {
|
||||
const existingIndex = resultKeyIndex.get(resultKey)
|
||||
if (existingIndex !== undefined) {
|
||||
if (resultSurfacePriority(message) > resultSurfacePriority(merged[existingIndex])) {
|
||||
merged[existingIndex] = message
|
||||
}
|
||||
continue
|
||||
}
|
||||
resultKeyIndex.set(resultKey, merged.length)
|
||||
}
|
||||
const dedupeKey = resultKey || uiMessageId || `${message.sender}:${message.replyToId ?? ''}:${message.timestamp}:${message.content.trim()}`
|
||||
if (seen.has(dedupeKey)) continue
|
||||
seen.add(dedupeKey)
|
||||
merged.push(message)
|
||||
}
|
||||
}
|
||||
return merged.sort((a, b) => (
|
||||
a.timestamp === b.timestamp
|
||||
? a.id.localeCompare(b.id)
|
||||
: a.timestamp - b.timestamp
|
||||
))
|
||||
}
|
||||
|
||||
export function mergeConversationProgressLog(timelineSessions: Session[]): ProgressEntry[] {
|
||||
const seen = new Set<string>()
|
||||
const merged: ProgressEntry[] = []
|
||||
for (const session of timelineSessions) {
|
||||
for (const entry of session.progressLog ?? []) {
|
||||
const dedupeKey = `${entry.timestamp}:${entry.type}:${entry.summary}:${entry.detail ?? ''}`
|
||||
if (seen.has(dedupeKey)) continue
|
||||
seen.add(dedupeKey)
|
||||
merged.push(entry)
|
||||
}
|
||||
}
|
||||
return merged.sort((a, b) => a.timestamp - b.timestamp)
|
||||
}
|
||||
|
||||
export function getConversationMessageCount(timelineSessions: Session[]): number {
|
||||
return timelineSessions.reduce(
|
||||
(total, session) => total + Math.max(0, session.messageCount ?? 0),
|
||||
0,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user