package server import ( "context" "fmt" "strings" "time" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/chat" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/taskrun" ) // projectContextConflictCards surfaces dialogue-context conflict confirmation // cards (HITL trigger_user route) to the user who triggered the run: // 1. a durable assistant message in the run's chat session (history APIs); // 2. the first card armed as PendingExpectation on the live session so a // follow-up「确认/纠正」hits the expectation branch. // // The adjudication ledger (context_conflict_adjudications) and the workspace // CONFIRMATIONS.md remain the authoritative record; this projection is UX. func (s *Server) projectContextConflictCards(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) { if s == nil || record == nil || result == nil || len(result.PendingContextConflicts) == 0 { return } if strings.TrimSpace(record.SessionID) == "" { return } content := renderContextConflictCardMessage(result.PendingContextConflicts) if cm := s.ctxMgr(); cm != nil { meta := map[string]any{ "run_id": record.RunID, "project_id": record.ProjectID, "client_turn_id": record.ClientTurnID, "ctx_conflict_card": true, } _, _ = cm.RecordUserMessageWithID(ctx, record.SessionID, 0, "assistant", content, "CTX_CONFLICT_CARD", "EXPECTATION_HIT", meta) } s.sessionsMu.RLock() sess := s.sessions[record.SessionID] s.sessionsMu.RUnlock() if sess == nil { return } if exp := bizdev.ContextConflictExpectation(result.PendingContextConflicts[0]); exp != nil && sess.UserCtx != nil { _ = coordinator.ApplyContextUpdate(sess.UserCtx, coordinator.ContextUpdate{ ExpectedVersion: sess.UserCtx.Version, SetPendingExpectation: true, PendingExpectation: exp, }) } sess.AppendMessage(chat.Message{ ID: fmt.Sprintf("msg_%d_cc", time.Now().UnixNano()), Role: "assistant", Content: content, Timestamp: time.Now(), }) } // renderContextConflictCardMessage builds the chat-visible confirmation card // text. Reply「确认」accepts the recommended winning value;「纠正」clears the // card and the user states the correct fact, which SUPPLEMENT_INFO absorbs. func renderContextConflictCardMessage(cards []bizdev.ContextConflictCard) string { var sb strings.Builder sb.WriteString("⚠️ 检测到项目对话与现有事实存在说法冲突,需要您确认:\n\n") for i, c := range cards { sb.WriteString(fmt.Sprintf("%d. %s\n 推荐采信:%s = %s\n 依据:%s\n", i+1, c.Prompt, c.FactKey, c.WinningValue, c.Snippet)) } sb.WriteString("\n回复「确认」采信推荐值(将同步项目黑板);回复「纠正」后直接告诉我正确的事实。") return sb.String() }