fix: distinguish missing historical timing from zero duration

This commit is contained in:
mesalogo
2026-10-05 02:45:40 +08:00
parent cf598121b1
commit d0877d9212
4 changed files with 37 additions and 3 deletions
@@ -14,6 +14,13 @@ requests remain paused during reconnection. Startup preserves closed intervals,
discards unclosed intervals, and marks them incomplete; old history is not
backfilled by scanning messages or events.
An incomplete zero duration means no known timed interval, not a measured zero.
Both conversation totals and task cards display "unavailable" in this case.
Positive partial durations retain the incomplete warning; complete zero durations
still display zero. The upgrade regression verifies that a new completed reply
in an old conversation retains its accumulated milliseconds after reopening the
database. This presentation fix does not restore pre-upgrade historical totals.
Schema 59 stores timing rows and adds a covering index for conversation
summaries. The old historical aggregation, its caches, dedicated reader/worker,
worker build entries, and five-second statistics polling have been removed.
@@ -240,6 +240,18 @@ describe('persisted running-state duration', () => {
expect(raw().prepare('PRAGMA index_info(messages_summary_idx)').all().map(row => row.name))
.toEqual(['conversation_id', 'sequence', 'role', 'created_at'])
expect(raw().prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
const replyId = task()
database.startExecutionTiming(replyId)
now += 7000
database.updateTaskStatus(replyId, 'completed')
database.endExecutionTiming(replyId)
expect(stats()).toMatchObject({ durationMs: 7000, runningCount: 0, incomplete: true })
database.close()
now += 86_400_000
database.initialize(directory)
expect(stats()).toMatchObject({ durationMs: 7000, runningCount: 0, incomplete: true })
expect(raw().prepare('SELECT duration_ms FROM execution_timing WHERE task_id = ?').get(replyId))
.toEqual({ duration_ms: 7000 })
})
it('deletes timing with its conversation and notifies snapshot subscribers', async () => {
@@ -454,6 +454,20 @@ describe('RightAssistantSidebar resizing', () => {
expect(screen.getByText('Project task a').closest('button')).toHaveAttribute('aria-current', 'true')
expect(screen.getByText('Project task b').closest('article')).not.toHaveTextContent('任务累计用时')
view.rerender(sidebarElement({ ...props,
conversationStats: { ...props.conversationStats, replyDurationMs: 0 },
taskDurations: new Map([['a', { durationMs: 0, incomplete: true }]])
}))
expect(stats.getByText('暂无统计')).toBeVisible()
expect(stats.queryByText('00:00:00')).not.toBeInTheDocument()
expect(screen.getByText('任务累计用时: 暂无统计 (部分记录)')).toBeVisible()
view.rerender(sidebarElement({ ...props,
conversationStats: { ...props.conversationStats, replyDurationMs: 500 },
taskDurations: new Map([['a', { durationMs: 500, incomplete: true }]])
}))
expect(stats.getByText('00:00:00')).toBeVisible()
expect(screen.getByText('任务累计用时: 00:00:00 (部分记录)')).toBeVisible()
view.rerender(sidebarElement({ ...props, activeConversationId: 'b' }))
expect(stats.queryByText('Conversation A')).not.toBeInTheDocument()
expect(stats.getAllByText('暂无统计')).toHaveLength(2)
+4 -3
View File
@@ -838,7 +838,8 @@ function RightAssistantSidebarView({
const currentStats = activeConversationId && conversationStats?.conversationId === activeConversationId
? conversationStats
: undefined
const formatDuration = (durationMs: number): string => {
const formatDuration = (durationMs: number, incomplete = false): string => {
if (incomplete && durationMs === 0) return t('sidebar.tasks.stats.unavailable')
const seconds = Math.floor(Math.max(0, durationMs) / 1000)
return `${Math.floor(seconds / 3600).toString().padStart(2, '0')}:${Math.floor(seconds / 60 % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`
}
@@ -2105,7 +2106,7 @@ function RightAssistantSidebarView({
? currentStats?.title ?? conversationTitles.get(activeConversationId) ?? t('sidebar.tasks.stats.currentConversation')
: t('sidebar.tasks.stats.noConversation')}</h3>
<dl className="task-center__stat-grid">
<div><dt>{t('sidebar.tasks.stats.replyDuration')}</dt><dd>{currentStats ? formatDuration(currentStats.replyDurationMs) : t('sidebar.tasks.stats.unavailable')}</dd></div>
<div><dt>{t('sidebar.tasks.stats.replyDuration')}</dt><dd>{currentStats ? formatDuration(currentStats.replyDurationMs, currentStats.incomplete) : t('sidebar.tasks.stats.unavailable')}</dd></div>
<div><dt>{t('sidebar.tasks.stats.messages')}</dt><dd>{currentStats ? currentStats.messageCount.toLocaleString(locale) : t('sidebar.tasks.stats.unavailable')}</dd></div>
</dl>
{currentStats?.incomplete && <p className="task-center__incomplete">{t('sidebar.tasks.stats.incomplete')}</p>}
@@ -2253,7 +2254,7 @@ function RightAssistantSidebarView({
</span>
<span>{t(`task.status.${task.status}`)}</span>
{duration && <span title={t('sidebar.tasks.stats.taskDuration')}>
{t('sidebar.tasks.stats.taskDuration')}: {formatDuration(duration.durationMs)}
{t('sidebar.tasks.stats.taskDuration')}: {formatDuration(duration.durationMs, duration.incomplete)}
{duration.incomplete && ` (${t('sidebar.tasks.stats.partial')})`}
</span>}
</div>