fix(ui): refine message visibility and goal approval flow

This commit is contained in:
JOJO 2026-05-30 16:30:05 +08:00
parent e220b3703a
commit 32865f6d6e
10 changed files with 320 additions and 44 deletions

View File

@ -141,6 +141,37 @@ from .goal_flow import (
emit_goal_progress,
)
_VALID_USER_MESSAGE_SOURCES = {
"user",
"guidance",
"notify",
"presend",
"sub_agent",
"background_command",
"goal",
"goal_prompt",
"goal_review",
"compression",
"permission",
"sandbox",
"skill",
}
def _user_message_ui_defaults(source: str, *, auto_user_message_event: bool = False) -> Dict[str, Any]:
normalized = str(source or "").strip().lower()
if normalized in {"skill", "goal_prompt", "permission", "sandbox"}:
return {"visibility": "hidden", "starts_work": False}
if normalized in {"guidance", "notify"}:
return {"visibility": "compact", "starts_work": False}
if normalized in {"goal_review", "compression", "sub_agent", "background_command"}:
return {"visibility": "compact", "starts_work": True}
if normalized == "presend":
return {"visibility": "chat", "starts_work": True}
if normalized == "goal":
return {"visibility": "compact", "starts_work": True}
return {"visibility": "chat", "starts_work": normalized == "user" and not auto_user_message_event}
def _should_skip_versioning_for_message(
*,
@ -308,7 +339,7 @@ async def _dispatch_completion_user_notice(
message_source = "background_command"
elif extra_payload.get("sub_agent_notice"):
message_source = "sub_agent"
if message_source not in {"user", "guidance", "notify", "presend", "sub_agent", "background_command", "goal"}:
if message_source not in _VALID_USER_MESSAGE_SOURCES:
message_source = "user"
try:
from .tasks import task_manager
@ -326,10 +357,17 @@ async def _dispatch_completion_user_notice(
"model_key": getattr(web_terminal, "model_key", None),
"message_source": message_source,
}
ui_defaults = _user_message_ui_defaults(
message_source,
auto_user_message_event=True,
)
# 关键:通知类后台任务需要把 user_message 写入任务事件流,
# 否则前端轮询只会看到 AI/tool 事件,看不到 user_message。
session_data["auto_user_message_event"] = True
session_data["auto_user_message_payload"] = dict(extra_payload or {})
session_data["auto_user_message_payload"] = {
**dict(extra_payload or {}),
**ui_defaults,
}
rec = task_manager.create_chat_task(
username,
workspace_id,
@ -346,8 +384,14 @@ async def _dispatch_completion_user_notice(
'conversation_id': conversation_id,
'task_id': rec.task_id,
'message_source': message_source,
**ui_defaults,
}
payload.update(extra_payload)
payload["metadata"] = {
"message_source": message_source,
**ui_defaults,
**(payload.get("metadata") or {}),
}
sender('user_message', payload)
return
except Exception as e:
@ -358,7 +402,14 @@ async def _dispatch_completion_user_notice(
'conversation_id': conversation_id,
'message_source': message_source,
}
payload.update(_user_message_ui_defaults(message_source, auto_user_message_event=True))
payload.update(extra_payload)
payload["metadata"] = {
"message_source": message_source,
"visibility": payload.get("visibility"),
"starts_work": payload.get("starts_work"),
**(payload.get("metadata") or {}),
}
sender('user_message', payload)
try:
task_handle = asyncio.create_task(handle_task_with_sender(
@ -789,7 +840,8 @@ async def handle_task_with_sender(
# 添加到对话历史
user_work_started_at = datetime.now().isoformat()
user_message_index = -1
user_work_finalized = False
current_work_user_message_index = -1
finalized_work_message_indices: set[int] = set()
history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or [])
is_first_user_message = history_len_before == 0
# 构建 user 消息来源与 metadata
@ -809,14 +861,20 @@ async def handle_task_with_sender(
user_message_source = "background_command"
elif auto_payload.get("sub_agent_notice"):
user_message_source = "sub_agent"
if user_message_source not in {"user", "guidance", "notify", "presend", "sub_agent", "background_command", "goal"}:
if user_message_source not in _VALID_USER_MESSAGE_SOURCES:
user_message_source = "user"
# 构建user消息metadata
user_message_metadata = {
"message_source": user_message_source,
}
if user_message_source in {"user", "presend", "sub_agent", "background_command"}:
user_message_metadata.update(
_user_message_ui_defaults(
user_message_source,
auto_user_message_event=bool(auto_user_message_event),
)
)
if user_message_metadata.get("starts_work") is True:
user_message_metadata["work_timer"] = {
"status": "working",
"started_at": user_work_started_at
@ -836,6 +894,7 @@ async def handle_task_with_sender(
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
except Exception:
user_message_index = -1
current_work_user_message_index = user_message_index
skill_context_messages = getattr(web_terminal, "_skill_context_messages", None)
if not auto_user_message_event and isinstance(skill_context_messages, list):
for skill_item in skill_context_messages:
@ -852,6 +911,8 @@ async def handle_task_with_sender(
metadata={
"source": "skill",
"message_source": "skill",
"visibility": "hidden",
"starts_work": False,
"hidden": True,
"skill_name": skill_name,
"skill_path": skill_path,
@ -867,6 +928,9 @@ async def handle_task_with_sender(
"videos": (saved_user_message or {}).get("videos") or videos or [],
"media_refs": (saved_user_message or {}).get("media_refs") or [],
"message_source": user_message_source,
"visibility": user_message_metadata.get("visibility"),
"starts_work": user_message_metadata.get("starts_work"),
"metadata": user_message_metadata,
"conversation_id": conversation_id,
},
)
@ -880,14 +944,14 @@ async def handle_task_with_sender(
auto_user_message_event=bool(auto_user_message_event),
)
def finalize_user_work_timer():
nonlocal user_work_finalized
if user_work_finalized:
def finalize_user_work_timer(index: Optional[int] = None):
target_index = current_work_user_message_index if index is None else index
if target_index in finalized_work_message_indices:
return
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
if user_message_index < 0 or user_message_index >= len(history):
if target_index < 0 or target_index >= len(history):
return
target_msg = history[user_message_index] or {}
target_msg = history[target_index] or {}
if target_msg.get("role") != "user":
return
metadata = target_msg.get("metadata") or {}
@ -910,9 +974,20 @@ async def handle_task_with_sender(
})
metadata["work_timer"] = timer
target_msg["metadata"] = metadata
history[user_message_index] = target_msg
history[target_index] = target_msg
web_terminal.context_manager.auto_save_conversation(force=True)
user_work_finalized = True
finalized_work_message_indices.add(target_index)
def switch_current_work_timer_to_latest_user():
nonlocal current_work_user_message_index
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
for idx in range(len(history) - 1, -1, -1):
msg = history[idx] or {}
if msg.get("role") == "user":
metadata = msg.get("metadata") or {}
if isinstance(metadata.get("work_timer"), dict):
current_work_user_message_index = idx
return
versioning_checkpoint_recorded = False
@ -1394,7 +1469,10 @@ async def handle_task_with_sender(
)
action = goal_result.get("action")
if action == "continue":
# 已注入续命 user 消息,重置目标轮段标志并继续主循环
# 已注入续命 user 消息:上一段 assistant work 到此结束,
# 后续计时切换到刚注入的 goal_review 消息。
finalize_user_work_timer()
switch_current_work_timer_to_latest_user()
goal_segment_made_tool_call = False
is_first_iteration = False
debug_log(f"[Goal] 续命:{goal_result.get('message', '')[:80]}")

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import time
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional
from modules.sub_agent_manager import TERMINAL_STATUSES
@ -15,9 +16,31 @@ _VALID_SOURCES = {
"sub_agent",
"background_command",
"goal",
"goal_prompt",
"goal_review",
"compression",
"permission",
"sandbox",
"skill",
}
def _runtime_message_ui_defaults(src: str, *, inline: bool = False) -> Dict[str, Any]:
"""Return UI metadata for model-facing user messages injected by the runtime."""
normalized = str(src or "").strip().lower()
if normalized in {"skill", "goal_prompt", "permission", "sandbox"}:
return {"visibility": "hidden", "starts_work": False}
if normalized in {"guidance", "notify"}:
return {"visibility": "compact", "starts_work": False}
if normalized in {"goal_review", "compression", "sub_agent", "background_command", "presend"}:
return {"visibility": "compact", "starts_work": True}
if normalized == "goal":
# Backward-compatible fallback for old callers. Inline goal injections are
# current-segment hints; non-inline goal injections start the next segment.
return {"visibility": "compact", "starts_work": not bool(inline)}
return {"visibility": "chat", "starts_work": normalized == "user"}
def inject_runtime_user_message(
*,
web_terminal,
@ -57,8 +80,14 @@ def inject_runtime_user_message(
"runtime_guidance": True,
"runtime_guidance_original": raw,
}
metadata.update(_runtime_message_ui_defaults(src, inline=inline))
if extra_metadata:
metadata.update({k: v for k, v in extra_metadata.items() if v is not None})
if metadata.get("starts_work") is True and not isinstance(metadata.get("work_timer"), dict):
metadata["work_timer"] = {
"status": "working",
"started_at": datetime.now().isoformat(),
}
if persist:
try:
@ -93,6 +122,9 @@ def inject_runtime_user_message(
"inline": inline,
"source": src,
"message_source": src,
"visibility": metadata.get("visibility"),
"starts_work": metadata.get("starts_work"),
"metadata": metadata,
"runtime_guidance": True,
"runtime_guidance_original": raw,
}

View File

@ -97,7 +97,7 @@ def inject_goal_prompt(
web_terminal=web_terminal,
messages=messages,
text=GOAL_MODE_PROMPT,
source="goal",
source="goal_prompt",
sender=sender,
conversation_id=conversation_id,
inline=False,
@ -231,7 +231,7 @@ async def handle_goal_after_turn(
web_terminal=web_terminal,
messages=messages,
text=f"{CONTINUE_PREFIX}{message}",
source="goal",
source="goal_review",
sender=sender,
conversation_id=conversation_id,
inline=False,

View File

@ -1,6 +1,7 @@
// @ts-nocheck
import { debugLog } from './common';
import { useTaskStore } from '../../stores/task';
import { getMessageVisibility, messageStartsWork } from '../../utils/messageVisibility';
const debugNotifyLog = (...args: any[]) => {
void args;
@ -114,6 +115,23 @@ function resolveUserMessageSource(data: any): string {
return 'user';
}
function resolveUserMessageMetadata(data: any, source: string, message: string): Record<string, any> {
const base = data && typeof data.metadata === 'object' && data.metadata ? { ...data.metadata } : {};
const metadata: Record<string, any> = {
...base,
message_source: source
};
if (data && Object.prototype.hasOwnProperty.call(data, 'visibility')) {
metadata.visibility = data.visibility;
}
if (data && Object.prototype.hasOwnProperty.call(data, 'starts_work')) {
metadata.starts_work = data.starts_work;
}
metadata.visibility = getMessageVisibility({ role: 'user', content: message, metadata, ...data });
metadata.starts_work = messageStartsWork({ role: 'user', content: message, metadata, ...data });
return metadata;
}
function isEmptyAssistantPlaceholderMessage(message: any): boolean {
if (!message || message.role !== 'assistant') {
return false;
@ -1912,10 +1930,6 @@ export const taskPollingMethods = {
this.autoApprovalFeedLines.push(String(progress.message));
}
this.autoApprovalFeedLines = this.autoApprovalFeedLines.slice(-20);
this.rightCollapsed = false;
if (this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
this.$forceUpdate();
},
@ -2044,6 +2058,7 @@ export const taskPollingMethods = {
const isAutoUserMessage = isSystemAutoUserMessagePayload(data);
const isRuntimeModeNotice = isRuntimeModeNoticePayload(data);
const source = resolveUserMessageSource(data);
const eventMetadata = resolveUserMessageMetadata(data, source, message);
const incomingImages = Array.isArray(data?.images) ? data.images : [];
const incomingVideos = Array.isArray(data?.videos) ? data.videos : [];
const incomingMediaRefs = Array.isArray(data?.media_refs)
@ -2072,6 +2087,7 @@ export const taskPollingMethods = {
target.media_refs = incomingMediaRefs;
target.metadata = {
...(target.metadata || {}),
...eventMetadata,
media_refs: incomingMediaRefs
};
} else {
@ -2080,7 +2096,8 @@ export const taskPollingMethods = {
incomingImages,
incomingVideos,
incomingMediaRefs,
source
source,
eventMetadata
);
}
this.taskInProgress = true;
@ -2099,6 +2116,9 @@ export const taskPollingMethods = {
messagePreview: message.slice(0, 80)
});
} else {
if (eventMetadata.starts_work === true) {
this.markLatestUserWorkCompleted();
}
const shouldRestoreWaitingPlaceholder =
this.moveTrailingEmptyAssistantPlaceholderAfterUserInsert('auto_user_message') &&
(this.taskInProgress || this.streamingMessage);
@ -2113,6 +2133,7 @@ export const taskPollingMethods = {
recentMatch.media_refs = incomingMediaRefs;
recentMatch.metadata = {
...(recentMatch.metadata || {}),
...eventMetadata,
media_refs: incomingMediaRefs,
message_source: source
};
@ -2122,7 +2143,8 @@ export const taskPollingMethods = {
incomingImages,
incomingVideos,
incomingMediaRefs,
source
source,
eventMetadata
);
}
if (shouldRestoreWaitingPlaceholder) {

View File

@ -6,11 +6,12 @@
:key="index"
class="message-block"
:class="{
'message-block--compact-user': getMessageVisibility(msg) === 'compact',
'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index)
}"
>
<div v-if="msg.role === 'user'" class="user-message">
<div v-if="msg.role === 'user'" class="user-message" :class="{ 'user-message--compact': getMessageVisibility(msg) === 'compact' }">
<div class="message-header icon-label">
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
<span>{{ userHeaderLabel(msg) }}</span>
@ -596,6 +597,7 @@ import ToolAction from '@/components/chat/actions/ToolAction.vue';
import StackedBlocks from './StackedBlocks.vue';
import MinimalBlocks from './MinimalBlocks.vue';
import { usePersonalizationStore } from '@/stores/personalization';
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
const props = defineProps<{
messages: Array<any>;
@ -677,15 +679,11 @@ function renderUserMessageContent(content: string): string {
html += escapeUserHtml(source.slice(cursor));
return html;
}
const isHiddenSkillMessage = (message: any) => {
const isHiddenUserMessage = (message: any) => {
if (!message || message.role !== 'user') {
return false;
}
const meta = message.metadata || {};
const source = String(meta.source || meta.message_source || '')
.trim()
.toLowerCase();
return source === 'skill' || meta.hidden === true;
return getMessageVisibility(message) === 'hidden';
};
const isEmptyAssistantMessage = (message: any) => {
if (!message || message.role !== 'assistant') {
@ -720,7 +718,7 @@ const filteredMessages = computed(() => {
if (m?.role === 'system') {
return false;
}
if (isHiddenSkillMessage(m)) {
if (isHiddenUserMessage(m)) {
return false;
}
if (isEmptyAssistantMessage(m)) {
@ -1353,18 +1351,15 @@ function findAssistantHeaderAnchorUser(index: number): any | null {
if (!prev || prev.role !== 'user') {
return null;
}
// assistant user
// guidance/goal user user 沿
// user
// assistant user-side
// starts_work=true work segment guidance
// segment
for (let i = index - 1; i >= 0; i -= 1) {
const item = filteredMessages.value[i];
if (!item || item.role !== 'user') {
break;
}
const source = String(item?.metadata?.message_source || 'user')
.trim()
.toLowerCase();
if (['user', 'presend', 'sub_agent', 'background_command'].includes(source)) {
if (messageStartsWork(item)) {
return item;
}
}

View File

@ -1,7 +1,7 @@
<template>
<aside
class="sidebar right-sidebar tool-approval-panel"
:class="{ collapsed }"
:class="{ collapsed, 'is-goal-approval-mode': isGoalApprovalMode }"
:style="{ width: collapsed ? '0px' : width + 'px' }"
>
<div class="sidebar-header" :class="{ 'mobile-header': isMobileViewport }">
@ -103,7 +103,11 @@
</div>
</div>
</div>
<div v-if="autoApprovalFeedLines.length || autoApprovalFinalMessage" class="auto-approval-block">
<div
v-if="autoApprovalFeedLines.length || autoApprovalFinalMessage"
class="auto-approval-block"
:class="{ 'auto-approval-block--goal': isGoalApprovalMode }"
>
<div v-if="!isGoalApprovalMode" class="auto-approval-block__title">{{ autoApprovalTitle }}</div>
<pre class="auto-approval-block__content">{{ autoApprovalFeedLines.join('\n') }}</pre>
<div v-if="autoApprovalFinalMessage" class="auto-approval-block__final">
@ -288,6 +292,22 @@ const parseFinalMessage = (text: string) => {
white-space: pre-wrap;
word-break: break-word;
}
.tool-approval-panel.is-goal-approval-mode .auto-approval-block {
background: var(--theme-surface-card, var(--theme-surface-soft));
border-color: var(--theme-control-border, var(--claude-border));
color: var(--claude-text);
}
.tool-approval-panel.is-goal-approval-mode .auto-approval-block__content,
.tool-approval-panel.is-goal-approval-mode .auto-approval-block__reason {
color: var(--claude-text-secondary);
}
.tool-approval-panel.is-goal-approval-mode .auto-approval-block__decision {
color: var(--claude-text);
}
:global(:root[data-theme='dark']) .auto-approval-block,
:global(body[data-theme='dark']) .auto-approval-block {
background: color-mix(in srgb, var(--theme-surface-muted, #141414) 86%, white 4%);

View File

@ -18,6 +18,7 @@
*/
import { io as createSocketClient } from 'socket.io-client';
import { renderLatexInRealtime } from './useMarkdownRenderer';
import { getMessageVisibility, messageStartsWork } from '../utils/messageVisibility';
export async function initializeLegacySocket(ctx: any) {
try {
@ -910,6 +911,36 @@ export async function initializeLegacySocket(ctx: any) {
: Array.isArray(data.mediaRefs)
? data.mediaRefs
: [];
const source = String(data?.message_source || data?.metadata?.message_source || 'user')
.trim()
.toLowerCase();
const eventMetadata = {
...(data?.metadata || {}),
message_source: source
};
if (Object.prototype.hasOwnProperty.call(data || {}, 'visibility')) {
eventMetadata.visibility = data.visibility;
}
if (Object.prototype.hasOwnProperty.call(data || {}, 'starts_work')) {
eventMetadata.starts_work = data.starts_work;
}
eventMetadata.visibility = getMessageVisibility({ role: 'user', content: message, metadata: eventMetadata, ...data });
eventMetadata.starts_work = messageStartsWork({ role: 'user', content: message, metadata: eventMetadata, ...data });
const isAutoUserMessage = !!(
data?.is_auto_generated ||
data?.metadata?.is_auto_generated ||
data?.auto_message_type ||
data?.metadata?.auto_message_type ||
data?.sub_agent_notice ||
data?.background_command_notice
);
if (
isAutoUserMessage &&
eventMetadata.starts_work === true &&
typeof ctx.markLatestUserWorkCompleted === 'function'
) {
ctx.markLatestUserWorkCompleted();
}
const last =
Array.isArray(ctx.messages) && ctx.messages.length
? ctx.messages[ctx.messages.length - 1]
@ -925,10 +956,11 @@ export async function initializeLegacySocket(ctx: any) {
last.media_refs = incomingMediaRefs;
last.metadata = {
...(last.metadata || {}),
...eventMetadata,
media_refs: incomingMediaRefs
};
} else {
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs);
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs, source, eventMetadata);
}
ctx.taskInProgress = true;
ctx.streamingMessage = false;

View File

@ -1,4 +1,5 @@
import { defineStore } from 'pinia';
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
interface ScrollStatePayload {
autoScrollEnabled?: boolean;
@ -184,17 +185,19 @@ export const useChatStore = defineStore('chat', {
images: string[] = [],
videos: string[] = [],
mediaRefs: Array<Record<string, any>> = [],
source: string = 'user'
source: string = 'user',
extraMetadata: Record<string, any> = {}
) {
const startedAt = new Date().toISOString();
const normalizedSource = String(source || 'user').trim().toLowerCase();
const shouldTrackWorkTimer = ['user', 'presend', 'sub_agent', 'background_command'].includes(
normalizedSource
);
const metadata: Record<string, any> = {
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
message_source: normalizedSource
message_source: normalizedSource,
...extraMetadata
};
metadata.visibility = getMessageVisibility({ role: 'user', content, metadata });
metadata.starts_work = messageStartsWork({ role: 'user', content, metadata });
const shouldTrackWorkTimer = metadata.starts_work === true;
if (shouldTrackWorkTimer) {
metadata.work_timer = {
status: 'working',

View File

@ -457,6 +457,10 @@
align-items: flex-end; /* 靠右对齐 */
}
.user-message.user-message--compact {
align-items: center;
}
.user-message .message-header,
.assistant-message .message-header {
font-size: 14px;
@ -496,6 +500,17 @@
align-self: flex-end; /* 确保靠右 */
}
.user-message.user-message--compact .message-text {
max-width: min(760px, 88%);
align-self: center;
padding: 10px 14px;
border-radius: 14px;
font-size: 13px;
color: var(--claude-text-secondary);
background: var(--theme-surface-card);
border: 1px solid var(--theme-control-border);
}
.user-message .message-text .user-skill-link {
color: #2563eb;
font-weight: 600;

View File

@ -0,0 +1,79 @@
export type MessageVisibility = 'chat' | 'compact' | 'hidden';
const VALID_VISIBILITY = new Set(['chat', 'compact', 'hidden']);
const LEGACY_STARTS_WORK_SOURCES = new Set(['user', 'presend', 'sub_agent', 'background_command']);
const COMPACT_FALLBACK_SOURCES = new Set([
'guidance',
'goal_review',
'compression',
'sub_agent',
'background_command'
]);
function normalizeSource(value: any): string {
return String(value || '').trim().toLowerCase();
}
function messageContent(message: any): string {
return String(message?.content || message?.message || '').trim();
}
function isLegacyGoalPrompt(message: any): boolean {
const content = messageContent(message);
return content.includes('【目标模式已开启】') || content.includes('[系统通知|goal]\n【目标模式已开启】');
}
function isLegacyGoalReview(message: any): boolean {
const content = messageContent(message);
return content.includes('审核智能体对于你的工作结束给出了以下内容');
}
export function getMessageVisibility(message: any): MessageVisibility {
const meta = message?.metadata || {};
const explicit = normalizeSource(meta.visibility || meta.ui?.visibility || message?.visibility);
if (VALID_VISIBILITY.has(explicit)) {
return explicit as MessageVisibility;
}
if (meta.hidden === true || meta.system_injected_image || meta.system_injected_video) {
return 'hidden';
}
const source = normalizeSource(meta.message_source || meta.source || message?.message_source || message?.source);
if (source === 'skill' || source === 'goal_prompt' || isLegacyGoalPrompt(message)) {
return 'hidden';
}
if (source === 'notify' && (meta.runtime_mode_notice || message?.runtime_mode_notice)) {
return 'hidden';
}
if (COMPACT_FALLBACK_SOURCES.has(source) || isLegacyGoalReview(message)) {
return 'compact';
}
return 'chat';
}
export function messageStartsWork(message: any): boolean {
const meta = message?.metadata || {};
if (typeof meta.starts_work === 'boolean') {
return meta.starts_work;
}
if (typeof meta.ui?.starts_work === 'boolean') {
return meta.ui.starts_work;
}
if (typeof message?.starts_work === 'boolean') {
return message.starts_work;
}
if (getMessageVisibility(message) === 'hidden') {
return false;
}
const source = normalizeSource(meta.message_source || meta.source || message?.message_source || message?.source);
if (source === 'guidance' || source === 'goal_prompt' || source === 'skill') {
return false;
}
if (source === 'goal_review' || source === 'compression' || isLegacyGoalReview(message)) {
return true;
}
return LEGACY_STARTS_WORK_SOURCES.has(source || 'user');
}