mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
fix(goal,ui): guard evaluating flag against the message_complete race
This commit is contained in:
parent
7d7ea99747
commit
34175ab94d
@ -546,12 +546,19 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
// Goal-evaluator breathing halo: when an assistant message finishes
|
// Goal-evaluator breathing halo: when an assistant message finishes
|
||||||
// and this conversation has an active goal, the backend's evaluation
|
// and this conversation has an active goal, the backend's evaluation
|
||||||
// node runs next. Flip the per-conv flag so GoalAvatarRing paints the
|
// node runs next. Flip the per-conv flag so GoalAvatarRing paints the
|
||||||
// breathing halo until `goal_evaluated` resets it. Skip when no goal
|
// breathing halo until `goal_evaluated` resets it.
|
||||||
// is active — the halo should be quiet for ordinary turns.
|
//
|
||||||
|
// Skip when:
|
||||||
|
// - the conversation has no active goal (ordinary turn, no halo)
|
||||||
|
// - the evaluator already fired in this turn (SSE order under the
|
||||||
|
// structured stream is goal_evaluated → done → message_complete,
|
||||||
|
// so re-arming here would leave the halo stuck on after the
|
||||||
|
// evaluator already cleared it)
|
||||||
if (
|
if (
|
||||||
data.status === 'completed'
|
data.status === 'completed'
|
||||||
&& streamConversationId
|
&& streamConversationId
|
||||||
&& goalStore.activeGoal(streamConversationId)
|
&& goalStore.activeGoal(streamConversationId)
|
||||||
|
&& !goalStore.recentlyEvaluated(streamConversationId)
|
||||||
) {
|
) {
|
||||||
goalStore.markEvaluating(streamConversationId, true)
|
goalStore.markEvaluating(streamConversationId, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,19 @@ export const useGoalStore = defineStore('goal', () => {
|
|||||||
// the message's `metadata.fromFollowup` flag (handled by ChatHistory).
|
// the message's `metadata.fromFollowup` flag (handled by ChatHistory).
|
||||||
const followupMessageIdsByConv = ref<Record<string, Set<string>>>({})
|
const followupMessageIdsByConv = ref<Record<string, Set<string>>>({})
|
||||||
|
|
||||||
|
// Timestamp (epoch ms) of the most recent terminal evaluator event for
|
||||||
|
// each conversation — used by useChat's message_complete handler to
|
||||||
|
// avoid re-setting the breathing-halo flag on the trailing edge of a
|
||||||
|
// turn whose evaluator already finished. Backend SSE order under the
|
||||||
|
// structured stream is goal_evaluated → done → message_complete, so
|
||||||
|
// without this guard the halo would re-light right after it cleared.
|
||||||
|
const lastTerminalEventAtByConv = ref<Record<string, number>>({})
|
||||||
|
/** Window during which message_complete suppresses re-setting evaluating
|
||||||
|
* after a goal_evaluated / goal_completed / goal_exhausted just landed.
|
||||||
|
* 4s leaves headroom for SSE jitter without blocking a genuine next
|
||||||
|
* turn from re-arming the halo. */
|
||||||
|
const TERMINAL_EVENT_SUPPRESSION_MS = 4000
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
async function loadActiveForConversation(conversationId: string) {
|
async function loadActiveForConversation(conversationId: string) {
|
||||||
@ -156,6 +169,7 @@ export const useGoalStore = defineStore('goal', () => {
|
|||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'goal_evaluated': {
|
case 'goal_evaluated': {
|
||||||
evaluatingByConv.value[conversationId] = false
|
evaluatingByConv.value[conversationId] = false
|
||||||
|
lastTerminalEventAtByConv.value[conversationId] = Date.now()
|
||||||
if (goal && data?.score != null) {
|
if (goal && data?.score != null) {
|
||||||
goal.completionScore = Number(data.score)
|
goal.completionScore = Number(data.score)
|
||||||
}
|
}
|
||||||
@ -175,6 +189,7 @@ export const useGoalStore = defineStore('goal', () => {
|
|||||||
}
|
}
|
||||||
case 'goal_completed': {
|
case 'goal_completed': {
|
||||||
evaluatingByConv.value[conversationId] = false
|
evaluatingByConv.value[conversationId] = false
|
||||||
|
lastTerminalEventAtByConv.value[conversationId] = Date.now()
|
||||||
// Capture the terminal snapshot BEFORE we null out the cache so
|
// Capture the terminal snapshot BEFORE we null out the cache so
|
||||||
// the GoalSystemLine has a title + score to render.
|
// the GoalSystemLine has a title + score to render.
|
||||||
recentTerminalByConv.value[conversationId] = {
|
recentTerminalByConv.value[conversationId] = {
|
||||||
@ -192,6 +207,7 @@ export const useGoalStore = defineStore('goal', () => {
|
|||||||
}
|
}
|
||||||
case 'goal_exhausted': {
|
case 'goal_exhausted': {
|
||||||
evaluatingByConv.value[conversationId] = false
|
evaluatingByConv.value[conversationId] = false
|
||||||
|
lastTerminalEventAtByConv.value[conversationId] = Date.now()
|
||||||
recentTerminalByConv.value[conversationId] = {
|
recentTerminalByConv.value[conversationId] = {
|
||||||
status: 'exhausted',
|
status: 'exhausted',
|
||||||
title: goal?.title || '目标',
|
title: goal?.title || '目标',
|
||||||
@ -228,6 +244,21 @@ export const useGoalStore = defineStore('goal', () => {
|
|||||||
evaluatingByConv.value[conversationId] = flag
|
evaluatingByConv.value[conversationId] = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a terminal evaluator event (goal_evaluated,
|
||||||
|
* goal_completed, goal_exhausted) arrived for this conversation
|
||||||
|
* within the suppression window — i.e. the evaluator has already
|
||||||
|
* finished this turn and the halo shouldn't be re-armed.
|
||||||
|
* Callers (useChat's message_complete handler) consult this before
|
||||||
|
* setting evaluating=true so the SSE order goal_evaluated → done →
|
||||||
|
* message_complete doesn't leave a permanently-lit ring.
|
||||||
|
*/
|
||||||
|
function recentlyEvaluated(conversationId: string): boolean {
|
||||||
|
const at = lastTerminalEventAtByConv.value[conversationId]
|
||||||
|
if (!at) return false
|
||||||
|
return Date.now() - at < TERMINAL_EVENT_SUPPRESSION_MS
|
||||||
|
}
|
||||||
|
|
||||||
function isEvaluating(conversationId: string): boolean {
|
function isEvaluating(conversationId: string): boolean {
|
||||||
return Boolean(evaluatingByConv.value[conversationId])
|
return Boolean(evaluatingByConv.value[conversationId])
|
||||||
}
|
}
|
||||||
@ -314,6 +345,7 @@ export const useGoalStore = defineStore('goal', () => {
|
|||||||
loadEvents,
|
loadEvents,
|
||||||
handleSseEvent,
|
handleSseEvent,
|
||||||
markEvaluating,
|
markEvaluating,
|
||||||
|
recentlyEvaluated,
|
||||||
isEvaluating,
|
isEvaluating,
|
||||||
activeGoal,
|
activeGoal,
|
||||||
progressFraction,
|
progressFraction,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user